23.2async/await and Futures

Last updated June 13, 2026

You know when to use async (lesson 23.1): many IO-bound waits at once. Now the syntax. Rust adds two keywords, async and await, and one underlying type, the Future. The single most important thing to understand, and the thing that trips up everyone arriving from other languages, is that an async function in Rust does nothing when you call it. It hands you a recipe, not a result, and the recipe only runs when something drives it. Once that clicks, the rest follows.

async makes a function return a Future

Put async in front of a function, and its return type quietly changes. This function looks like it returns a u32:

async fn compute() -> u32 {
    42
}

But calling compute() does not give you 42, and it doesn't run the body. It gives you a Future, a value representing a computation that will produce a u32 when driven to completion. A Future is lazy in exactly the way an iterator is lazy (chapter 19): building it does nothing; you have to consume it. Watch the difference:

async fn compute() -> u32 {
    println!("computing!");
    42
}

fn main() {
    let fut = compute();        // body does NOT run; no "computing!" printed
    println!("made a future, ran nothing");
}
made a future, ran nothing

compute() returned a Future and fut holds it, but "computing!" never prints, because the body never ran. The function call built the recipe and stopped. This is the chapter-19 laziness lesson again: just as a map adapter does nothing until consumed, an async function does nothing until its Future is driven. If you come from JavaScript or Python, where calling an async function starts it, this is the surprise to unlearn: in Rust, futures are inert until polled.

.await drives a Future, but only inside async

So how do you get the 42 out? You .await the future. .await says "run this future to completion and give me its value, and while it's waiting, let other tasks use this thread." But there's a catch with teeth: you can only use .await inside an async function. An async function awaiting another async function is the normal shape:

async fn compute() -> u32 {
    42
}

async fn run() {
    let n = compute().await;   // drive the future, get the u32
    println!("got {n}");
}

Inside run (which is async), compute().await runs the future and binds 42 to n. The .await is the point where, if compute had to wait for something, run's task would step aside and let other tasks proceed (lesson 23.1). Awaiting is both "get the result" and "here's a place I'm willing to yield."

But notice the problem this creates. .await only works inside async. So run must be async to await compute. And whoever calls run must .await it, so they must be async too. The async-ness propagates outward, function by function, all the way up. This is the famous function coloring: async functions can only be awaited by other async functions, so "async" spreads through a call chain like a dye. It's one of the real costs the last lesson warned about, and it's why making one function async tends to pull its callers along.

The chain has to stop somewhere: main can't await

Follow the coloring up and you hit a wall. main is the top, and main is not async, you can't write let n = run().await in an ordinary main, because main isn't an async function and .await is forbidden outside one. So how does the first future ever get driven? Something at the very top must take a future and run it to completion through plain, non-async means. That something is a runtime, and Rust, deliberately, does not ship one.

async fn run() {
    println!("running!");
}

fn main() {
    let fut = run();
    // fut is a Future. Nothing drives it. "running!" never prints.
    // We can't .await here: main isn't async.
}

This compiles, prints nothing, and even warns that the future is unused, the laziness biting one more time. To actually run run(), you need an engine that accepts a future and polls it to completion, parking and waking it as its waits resolve. That engine is the async runtime, the subject of the next lesson.

Why Rust ships no runtime, and what that means for you

Most languages bake one async runtime into the standard library. Rust deliberately doesn't: it standardizes the async/await syntax and the Future trait, but leaves the actual executor, the thing that runs futures, to libraries, so different domains (network servers, embedded devices, GUIs) can pick a runtime suited to them. The upside is flexibility; the very visible downside is that you must add a runtime dependency to run a single line of async code. There's no built-in block_on. This is a real seam, and a frequent source of the beginner question "why won't my async code run?" The answer is almost always "you haven't given it a runtime yet."

Futures are a trait, briefly

Under the syntax, a Future is a trait with one method, poll, that the runtime calls repeatedly: each call either returns Ready(value) (done) or Pending (not yet, try again when I signal). You will essentially never call poll or implement Future by hand, the async/await syntax generates all of it for you, the same way for generates next calls (chapter 19). It's worth knowing the trait is there, because it explains the model: the runtime drives your futures by polling them, and .await is sugar for "poll this until Ready, yielding on Pending." But day to day, you write async fn and .await, and the machinery stays out of sight.

Quiz time

Question #1

What does calling an async fn actually do, and what does it not do?

Show solution

Calling an async fn builds and returns a Future (a lazy value representing the eventual result). It does not run the function body or produce the result, no code in the body executes yet. A Future is inert until something drives it (via .await or a runtime), exactly like an iterator adapter is lazy until consumed. This surprises people coming from languages where calling an async function starts it running.

Question #2

What does .await do, and why can you only use it inside an async function?

Show solution

.await drives a future to completion and yields its value, and at that point it allows the task to step aside (yield the thread) while waiting. It's only allowed inside async functions because awaiting is the mechanism by which an async function suspends and resumes; a normal function has no such suspension machinery. This is why async "colors" functions: to await something, the caller must itself be async, so async-ness propagates up the call chain.

Question #3

Why does async code "not run" if you just call an async function from main, and what's missing?

Show solution

Calling the async function only creates a Future, which is lazy and does nothing until driven. You can't .await it in main because main isn't async. What's missing is a runtime (executor): the engine that takes a future and polls it to completion through non-async means. Rust deliberately ships no built-in runtime, so you must add one (a dependency like Tokio) before any async code will actually execute.

You have the syntax and you've hit the wall: nothing runs without a runtime. The next lesson (23.3) brings in Tokio, by far the most common async runtime, and finally makes async code execute, with a small concurrent fetcher to show what it's for.