Async functions and lazy futures
Async functions and lazy futures
A service may spend time waiting for I/O. Async Rust represents work as a future that can make progress when polled. An async function call creates a future; it does not automatically start a thread or run the function body. This distinction matters before choosing an executor library.
What you’ll learn
- Distinguish future creation, polling and body execution.
- Explain why async needs an appropriate executor for real work.
Async functions and lazy futures in action
Create future: body not run
Calling report creates a future.
Create future: body not runNo executor polls it
Main prints the creation message, then drops it without an executor polling it.
No executor polls itDrop: no score produced
Neither report nor load_score runs its body, so Body executed never prints.
Drop: no score produced
Async functions and lazy futures explained
.await waits for another future within an async context, allowing the executor to schedule other ready work when the current future is pending. The standard library defines Future but does not provide a general application executor. The complete program below deliberately creates and drops a future to demonstrate that construction is not execution.
Connect this idea with cargo packages and dependencies and threads, ownership and joining.
Work through the Rust program
Save the complete source as main.rs in the Rust environment. No standard input is needed.
Rust 2024 · complete program
async fn load_score() -> u32 {
println!("Body executed");
80
}
async fn report() -> u32 { load_score().await }
fn main() {
let pending = report();
println!("Future created");
drop(pending);
println!("Future dropped without polling");
}Expected output
Future created
Future dropped without pollingFollow the execution
Calling report creates a future. Main prints the creation message, then drops it without an executor polling it. Neither report nor load_score runs its body, so Body executed never prints. This is a deliberate non-execution demonstration, not a completed asynchronous fetch.
Try the complete program in the Rust compiler. For tests, Cargo commands and local files, use the specified local environment.
Choosing async work versus a thread
Async is useful when many operations spend time waiting and their libraries cooperate with a runtime. A thread is a separately scheduled execution path and may be a simpler choice for a blocking library. CPU-heavy work still needs execution time; declaring a function async does not make that work parallel or faster.
Before a real asynchronous network example, choose an executor, add its dependency to Cargo.toml and follow its runtime entry-point instructions. This course’s lazy-future program intentionally needs no third-party runtime.
Drive one immediately ready future
The previous example intentionally never ran the async body. Here is the complete code for a different case: a future whose result is immediately ready. Poll asks the future to make progress. A Context supplies a waker, which a pending future can use to request another poll. Box::pin keeps the future at a stable allocated location while it is polled.
Rust 2024 · complete program
use std::{future::{self, Future}, task::{Context, Poll, Waker}};
async fn load_score() -> u32 { future::ready(80).await }
async fn report() -> u32 { load_score().await }
fn main() {
let mut task = Box::pin(report());
let mut context = Context::from_waker(Waker::noop());
match task.as_mut().poll(&mut context) {
Poll::Ready(score) => println!("Recorded: {score}"),
Poll::Pending => println!("Not ready yet"),
}
}Expected output
Recorded: 80Main polls report once. Report awaits load_score, which awaits an immediately ready value. The calls return 80 in the same poll, producing Poll::Ready(80). Nothing in this example starts a thread or waits for actual I/O.
This is a one-poll teaching demonstration, not a general executor. Waker::noop does not arrange another poll. A genuinely pending I/O future needs appropriate scheduling and wakeups; repeatedly polling without that design is not a replacement for a runtime. See the Future polling contract and the Tokio runtime tutorial.
Try a small variation
Your task: Does calling report() automatically print Body executed?
Rust 2024 · complete program
async fn load_score() -> u32 {
println!("Body executed");
80
}
async fn report() -> u32 { load_score().await }
fn main() {
let pending = report();
println!("Future created");
drop(pending);
println!("Future dropped without polling");
}Worked answer
No. The returned future must be driven by an executor or another polling context. Merely creating it does not execute the async body.
Runtime and cancellation boundaries
Executors such as Tokio provide their own setup and runtime APIs; use documentation for the chosen runtime. Blocking I/O can still block an executor thread. Dropping futures is not a universal rollback or cancellation guarantee for work already performed or separately spawned.
Check your understanding
Take the idea into your next program
No. The returned future must be driven by an executor or another polling context. Merely creating it does not execute the async body. Executors such as Tokio or async-std provide their own setup and runtime APIs; use documentation for the chosen runtime.
Further reading: the official Rust Book on async functions and lazy futures.
Reaching the recap and completing the quizzes are separate milestones.