Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

async Blocks

Goal of This Episode

Learn to make a Future on the spot inside a function with async { ... }, and understand its relationship to async fn.

Main Text

Making a Future on the Spot

Besides async fn, Rust also lets you create a Future on the spot, mid-program, with async { ... }:

extern crate tokio;

#[tokio::main]
async fn main() {
    // this async block is itself a Future
    let fut = async {
        println!("I'm inside an async block");
        42
    };

    // just like an async fn, it only runs when .awaited
    let value = fut.await;
    println!("got {}", value);
}

Note: exactly like an async fn, merely writing async { ... } doesn’t execute its contents — you’ve only made a lazy Future, and it makes progress only when .awaited.

How async fn and async blocks Relate

A rough first cut at telling them apart:

  • An async fn is a named Future factory — define it once, call it repeatedly, each call producing a fresh Future.
  • An async block is an anonymous Future created on the spot — right here, just this one, no name.

That is, looking only at “named and reusable” versus “anonymous and on the spot,” they’re a bit like the difference between ordinary functions and closures. But that’s just a first-impression analogy — don’t read too much into it: async fns and async blocks both produce Futures, but an async block is not a closure; you don’t call it with (). Once created, it’s driven by .await or the runtime.

async move

An async block can also be written as async move { ... }. The move here is similar to move on a closure: it moves the outside variables used by the block into the Future. What gets moved is the variable itself; if that variable is already a reference, then the reference is what gets moved.

extern crate tokio;

#[tokio::main]
async fn main() {
    let name = String::from("Ferris");

    let fut = async move {
        println!("hello, {}", name);
    };

    fut.await;

    // println!("{}", name); // compile error: name has been moved into the async block
}

Without move, an async block usually tries to borrow outside variables. With async move, the outside variables it uses are moved into the resulting Future. This is common when you want to store a Future, hand it to a runtime, or let it leave the current scope.

In the Result World, This Needs No New Syntax

Here’s an interesting contrast. In the Result world, if you want “a block right here where I can use ?,” you need no new syntax at all — an immediately invoked closure does it:

fn main() {
    // define a closure, then call it immediately with ()
    let result: Result<i32, std::num::ParseIntError> = (|| {
        let x = "3".parse::<i32>()?;
        let y = "4".parse::<i32>()?;
        Ok(x + y)
    })();

    println!("{:?}", result);
}

The (|| { ... })() here means “define a closure and call it immediately.” The closure’s body can use ? because the closure itself returns a Result; after the call, the outer main simply receives that Result value.

Why the Future World Can’t Copy That Trick

You might wonder: can the Future world just do the same? Stuff the .await into an immediately invoked closure?

extern crate tokio;

async fn get_number() -> i32 {
    42
}

#[tokio::main]
async fn main() {
    let value = (|| {
        get_number().await // compile error: can't .await in an ordinary closure
    })();
}

No. The reason goes back to the previous episodes: .await requires the whole stretch of code to be rewritten into a state machine so it can “pause to allow concurrency.” But an ordinary closure compiles into an ordinary function, which has no notion of “pause now, resume later” — it can’t express that rewrite. So the Result world’s trick doesn’t carry over.

This is exactly why async blocks exist. Writing async { ... } explicitly tells the compiler: “rewrite this block into a Future.” With that dedicated syntax in place, .await becomes legal inside:

extern crate tokio;

async fn get_number() -> i32 {
    42
}

#[tokio::main]
async fn main() {
    let value = async {
        get_number().await // works this time, because this is an async block
    }.await;
    println!("{}", value);
}

With that, the first five episodes have laid down the basic syntax and mental models — async fn, .await, async blocks. Starting next episode, we roll up our sleeves and take the internals of Future apart with our own hands.

Recap

  • async { ... } creates an anonymous Future on the spot, mid-function; it likewise runs only when .awaited.
  • An async fn is a named, reusable Future factory; an async block is one anonymous Future made in place.
  • async move { ... } moves the outside variables it uses into the resulting Future; this is common when the Future needs to leave the current scope or be handed to a runtime.
  • An immediately invoked closure that returns Result can use ? inside; the outer code just receives the closure call’s Result value.
  • .await can’t copy that trick — it may only appear inside async constructs, and an ordinary closure can’t pause and resume — hence the dedicated async block syntax.