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
- Add a second function that returns a string and await both in one async block.
- Print the size of a future with size_of_val before and after adding an await.
- 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.
-
What does calling an
async fndo?Answer
It returns a future without running the body
The body runs when the future is polled, not when it is created.
-
What does
awaitdo 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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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]:
awaitis only allowed insideasyncfunctions and blocksOnly an async body can suspend, so the keyword is refused in a plain function.
-
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 implementstd::fmt::DisplayA 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