ProgramGuru

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

  1. Create future: body not run

    Calling report creates a future.

    Create future: body not run
  2. No executor polls it

    Main prints the creation message, then drops it without an executor polling it.

    No executor polls it
  3. Drop: no score produced

    Neither report nor load_score runs its body, so Body executed never prints.

    Drop: no score produced
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.

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 polling

Follow 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: 80

Main 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

Does calling report() automatically print Body executed?
Choose one answer. You can try again.

Answer and explanation

No.

No. The returned future must be driven by an executor or another polling context. Merely creating it does not execute the async body.

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.

    Next lesson: Build a validated student report →