Async Introduction
async and .await
Rustβs async model is similar to C#: you mark code as async and use .await to wait on futures without blocking the thread.
async fn hello() -> String {
"Hello".to_string()
}
#[tokio::main]
async fn main() {
let s = hello().await;
println!("{}", s);
}In C#: Like async Task<string> Hello() => "Hello"; and await Hello();. In C#, the runtime is built-in (.NET); in Rust you use a runtime like Tokio or async-std.
Future
An async fn returns a Future. The future is lazy: it does nothing until awaited. Only when you .await it does the work run (on the async runtime).
In C#: This is a key difference. A C# Task is "hot" β calling an async method starts running it immediately (up to the first await), without waiting to be awaited. A Rust Future is "cold"/lazy β calling an async fn only builds the future; nothing runs until you .await it (or hand it to the runtime, e.g. via tokio::spawn).
Tokio runtime
Tokio is the most common async runtime. You need #[tokio::main] (or a custom runtime) to run async code. The main function becomes async fn main() and Tokio runs it.
#[tokio::main]
async fn main() {
let result = some_async_fn().await;
}In C#: The .NET thread pool and sync context run async code; you donβt install a separate runtime. In Rust you add Tokio (or similar) as a dependency.
Returning Result from async main and using ?
main can return a Result, which lets you use the ? operator directly in async code β exactly like in a synchronous fn main() -> Result<...>. When an .awaited call returns a Result, ? either unwraps the Ok value or returns early with the error:
use std::error::Error;
async fn parse_count() -> Result<u32, std::num::ParseIntError> {
"42".parse::<u32>()
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let n: u32 = parse_count().await?; // unwrap Ok, or return the error from main
println!("got {} items", n);
Ok(())
}Box<dyn std::error::Error> is a catch-all error type: ? auto-converts any concrete error (here ParseIntError) into it via the From trait, so one main signature accepts errors from many different calls. If main returns an Err, the process exits with a non-zero status and prints the error via Debug.
In C#: async Task<int> Main() lets you await in Main, but C# has no error-propagation ? operator β you propagate failures by letting exceptions bubble up (or returning a result type manually). In Rust, ? is the idiomatic way to short-circuit on errors, and it works the same in async and sync functions.
Later async and backend modules (HTTP clients, Postgres, Axum handlers, Redis) lean on this constantly: almost every I/O call returns a Result, and ? lets you chain connect().await?, query(...).await?, send().await? without nested match blocks. Returning Result from main is the simplest way to make ? usable at the top level.
Key takeaway
Use async fn and .await; add a runtime (e.g. Tokio) to execute async code. Make main return Result<(), Box<dyn Error>> so you can use ? to propagate errors from .awaited calls β the pattern every later I/O-heavy module relies on. Conceptually similar to C# async/await; the main differences are the explicit runtime and the ? operator in Rust.