๐ŸŽ“ A1Learner
๐Ÿ”

Rust Async Syntax

Async syntax is the spelling for building a future and waiting for its result. The word async marks a function or a block.

async fn: calling it builds a future

Use async fn to define a function that returns a future instead of running at once:

// Cargo.toml: tokio = { version = "1", features = ["full"] }
async fn greet(name: &str) -> String {
    format!("hello, {name}")
}

#[tokio::main]
async fn main() {
    let future = greet("Ferris"); // nothing has run yet
    let word = future.await; // now it runs
    println!("{word}");
}

async move: the block owns what it uses

Use async { ... } to make a future from a block. Use async move { ... } when the block must own the values it uses:

// Cargo.toml: tokio = { version = "1", features = ["full"] }
#[tokio::main]
async fn main() {
    let word = String::from("hi");
    let job = async move { word.len() }; // move: the block owns word
    println!("len = {}", job.await);
}

await sits after the value, so chains read in order

await is postfix: it goes after the future, as in fetch().await โ€” never before it. A chain like get(url).await.text().await waits twice, from left to right:

// Cargo.toml: tokio = { version = "1", features = ["full"] }
async fn greet(name: &str) -> String {
    format!("hello, {name}")
}

#[tokio::main]
async fn main() {
    let n = greet("Ferris").await.len(); // await first, then .len() on the result
    println!("letters = {n}");
}

main cannot be async

Something must start the runtime, and main is that starting point. Runtimes give you a macro or a block_on call instead:

async fn main() { }
// error E0752: `main` has an invalid return type

Example

// Cargo.toml: tokio = { version = "1", features = ["full"] }
async fn greet(name: &str) -> String {
    format!("hello, {}", name)
}

#[tokio::main]
async fn main() {
    // .await sits after the call, so calls can chain.
    let word = greet("Ferris").await.len();
    let job = async move { word * 2 };
    println!("letters = {}", word);
    println!("doubled = {}", job.await);
}

Key points

  • async fn is shorthand.
  • It compiles to a plain function returning impl Future<Output = T>.
  • Here T is the return type you wrote.
  • The compiler turns an async body into a state machine.
  • Each await point becomes a saved place.
  • The runtime can resume there later.
  • In many other languages, await goes before the value, like a command.

In this example, greet is an async fn, so calling it gives a future and runs nothing yet. In main, greet("Ferris").await runs that future and hands back the String hello, Ferris. Because await sits after the call, .len() chains straight onto the result and counts 13 letters, saved as word. The async move block then takes ownership of word and doubles it. Awaiting that small future gives 26. The #[tokio::main] attribute supplies the runtime, because main itself is not allowed to be async.

Note

In Rust, await sits after the value, like the ? marker on a Result, so method chains stay smooth.

Try it Yourself

Edit the code, then press Run.

Running is simulated ยท Rust Playground

Exercise

Why does async fn main() fail to compile?