async and await

An async fn returns a value of a type that implements Future, and calling it does not run the body. The body runs when the future is polled, which an executor does on the program's behalf. Rust ships the trait and the syntax and leaves the executor to a crate, which is why every program on this page carries a small one of its own.

This lesson builds that driver in twenty lines, then uses it to show the order of await, a nested call, and a future that is dropped before it ever runs. The last section is the compiler refusing an await outside an async context.

A future needs a driver

block_on pins the future, makes a context with a waker that does nothing and polls in a loop until the future is ready. A real executor parks the thread instead of yielding, which is the subject of the next lesson.

The program drives an async function that returns forty-two. The function has no await in it, so the first poll finishes the whole body.

use std::future::Future; use std::pin::pin; use std::task::{Context, Poll, Waker}; fn block_on<F: Future>(future: F) -> F::Output { let mut future = pin!(future); let waker = Waker::noop(); let mut context = Context::from_waker(waker); loop { match future.as_mut().poll(&mut context) { Poll::Ready(value) => return value, Poll::Pending => std::thread::yield_now(), } } } async fn answer() -> i32 { 42 } fn main() { println!("{}", block_on(answer())); }

rustc --edition 2024 drive.rs
./drive

42

The pin! is required because a future may be polled through a pointer to a value that must not move; the macro creates the pinned binding on the stack. The waker is what a future uses to ask for another poll, and the no-op one is enough while the loop keeps polling anyway.

Await runs in order

An await runs the awaited future to completion and continues with its value, so the statements of an async block execute in the order a reader expects. Nothing runs before the first poll, which the print before the driver makes visible.

The program prints a line before the driver, then the two steps and the line between them. The sum comes last.

use std::future::Future; use std::pin::pin; use std::task::{Context, Poll, Waker}; fn block_on<F: Future>(future: F) -> F::Output { let mut future = pin!(future); let waker = Waker::noop(); let mut context = Context::from_waker(waker); loop { match future.as_mut().poll(&mut context) { Poll::Ready(value) => return value, Poll::Pending => std::thread::yield_now(), } } } async fn step(name: &str, value: i32) -> i32 { println!("run {name}"); value } fn main() { let future = async { let a = step("first", 1).await; println!("between"); let b = step("second", 2).await; a + b }; println!("before"); println!("{}", block_on(future)); }

rustc --edition 2024 order.rs
./order

before
run first
between
run second
3

The order is a property of the code and not of the executor: await is a suspension point, and the statements around it run in sequence. A program that joins two independent futures with join! runs them concurrently, and that macro comes from a crate, not from the standard library.

An async block captures the names it mentions, and a future that is sent to another thread has to be Send, which is why an await point may not hold a reference into a thread-local. The compiler checks that bound at the spawn, and the message names the type that broke it.

One async function calling another

An async function can await another one, and the result is a future that contains the inner future as a field. The types grow with the nesting, which is why the compiler builds a state machine for each async body instead of allocating a stack frame.

The program awaits a doubling function from a formatting function and drives the pair with one block_on. The output is the formatted number.

use std::future::Future; use std::pin::pin; use std::task::{Context, Poll, Waker}; fn block_on<F: Future>(future: F) -> F::Output { let mut future = pin!(future); let waker = Waker::noop(); let mut context = Context::from_waker(waker); loop { match future.as_mut().poll(&mut context) { Poll::Ready(value) => return value, Poll::Pending => std::thread::yield_now(), } } } async fn double(value: i32) -> i32 { value * 2 } async fn labelled(value: i32) -> String { let doubled = double(value).await; format!("value {doubled}") } fn main() { println!("{}", block_on(labelled(21))); }

rustc --edition 2024 nested.rs
./nested

value 42

Only the outer future is polled by the driver; the inner one is polled by the outer body at the await. That chain of calls is what the next lesson instruments with a counter, and it is why a future has to be polled until it is ready rather than just once.

A future that never runs

A future does nothing until it is polled, so a future that is dropped has no effect at all. That is also the reason a task can be cancelled by dropping its handle: the body runs up to the last completed await and stops.

The program creates a future that would print, drops it without polling, and then drives a different one. Only the second line of work appears.

use std::future::Future; use std::pin::pin; use std::task::{Context, Poll, Waker}; fn block_on<F: Future>(future: F) -> F::Output { let mut future = pin!(future); let waker = Waker::noop(); let mut context = Context::from_waker(waker); loop { match future.as_mut().poll(&mut context) { Poll::Ready(value) => return value, Poll::Pending => std::thread::yield_now(), } } } async fn work() { println!("work runs"); } fn main() { let future = work(); drop(future); println!("{}", block_on(async { 7 })); }

rustc --edition 2024 dropped.rs
./dropped

7

The output has no work runs, because nothing polled that future. This is the difference from a thread: a thread starts on its own, and a future starts when someone drives it. The lesson on threads is the other model.

Await outside async

await may only appear inside an async function or block, because only there does the compiler generate the state machine that can suspend. A plain main cannot, and the error is E0728.

The program awaits in main. The compiler points at the keyword and at the function that is not async.

async fn answer() -> i32 { 42 } fn main() { let value = answer().await; println!("{value}"); }

rustc --edition 2024 await_err.rs

error[E0728]: `await` is only allowed inside `async` functions and blocks
--> await_err.rs:6:26
|
5 | fn main() {
| --------- this is not `async`
6 | let value = answer().await;
| ^^^^^ only allowed inside `async` functions and blocks

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0728`.

The fix is the driver of the first section: a plain function calls block_on and the async part lives inside it. The standard library has no executor because the runtime a program needs — a reactor, a timer, a thread pool — belongs with the program, and a crate provides it. The next lesson writes the driver properly and names tokio.

Exercises

  1. Add a second function that returns a string and await both in one async block.
  2. Print the size of a future with size_of_val before and after adding an await.
  3. Drop a future in the middle of a loop and check that its body never printed.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What does calling an async fn do?

    Answer

    It returns a future without running the body

    The body runs when the future is polled, not when it is created.

  2. What does await do to the current task?

    Answer

    It suspends it until the awaited future is ready

    The task yields at the await and resumes with the value when the future is ready.

  3. Why does the standard library have no executor?

    Answer

    The runtime a program needs is a choice, and a crate provides it

    Rust ships the Future trait and leaves the reactor and the scheduler to crates.

  4. What happens to a future that is dropped before it is polled?

    Answer

    Its body never runs

    Nothing pulls the future, so the state machine never advances.

  5. What does this program print? use std::future::Future; use std::pin::pin; use std::task::{Context, Poll, Waker}; fn block_on<F: Future>(future: F) -> F::Output { let mut future = pin!(future); let waker = Waker::noop(); let mut context = Context::from_waker(waker); loop { match future.as_mut().poll(&mut context) { Poll::Ready(value) => return value, Poll::Pending => std::thread::yield_now(), } } } async fn answer() -> i32 { 6 * 7 } fn main() { println!("{}", block_on(answer())); }

    Answer

    42

    The driver polls the future once and the body returns forty-two.

  6. What does this program print? use std::future::Future; use std::pin::pin; use std::task::{Context, Poll, Waker}; fn block_on<F: Future>(future: F) -> F::Output { let mut future = pin!(future); let waker = Waker::noop(); let mut context = Context::from_waker(waker); loop { match future.as_mut().poll(&mut context) { Poll::Ready(value) => return value, Poll::Pending => std::thread::yield_now(), } } } async fn work() { println!("work"); } fn main() { let future = work(); drop(future); println!("{}", block_on(async { 7 })); }

    Answer

    7

    The dropped future never runs, so only the driver of the second one prints.

  7. What does this program print? use std::future::Future; use std::pin::pin; use std::task::{Context, Poll, Waker}; fn block_on<F: Future>(future: F) -> F::Output { let mut future = pin!(future); let waker = Waker::noop(); let mut context = Context::from_waker(waker); loop { match future.as_mut().poll(&mut context) { Poll::Ready(value) => return value, Poll::Pending => std::thread::yield_now(), } } } async fn one() -> i32 { 1 } async fn two() -> i32 { 2 } fn main() { let sum = block_on(async { one().await + two().await }); println!("{sum}"); }

    Answer

    3

    The two futures are awaited in sequence and their values are added.

  8. What does this program print? use std::future::Future; use std::pin::pin; use std::task::{Context, Poll, Waker}; fn block_on<F: Future>(future: F) -> F::Output { let mut future = pin!(future); let waker = Waker::noop(); let mut context = Context::from_waker(waker); loop { match future.as_mut().poll(&mut context) { Poll::Ready(value) => return value, Poll::Pending => std::thread::yield_now(), } } } async fn label(value: i32) -> String { format!("value {value}") } fn main() { println!("{}", block_on(label(5))); }

    Answer

    value 5

    The async function builds the string and the driver returns it.

  9. What is the first line of the compiler output for this program? async fn answer() -> i32 { 42 } fn main() { let value = answer().await; println!("{value}"); }

    Answer

    error[E0728]: await is only allowed inside async functions and blocks

    Only an async body can suspend, so the keyword is refused in a plain function.

  10. What is the first line of the compiler output for this program? async fn answer() -> i32 { 42 } fn main() { let future = answer(); println!("{}", future); }

    Answer

    error[E0277]: impl Future<Output = i32> doesn't implement std::fmt::Display

    A future is not the value; it has to be awaited or driven before it can be printed.

The code on this page was built with rustc 1.96.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev