Skip to content

Q058. Why are Rust futures lazy?

Short Answer

Rust futures make progress only when polled. Calling an async fn constructs a future but does not independently schedule it. Laziness allows futures to be composed without immediately starting work, avoids requiring one global runtime, and makes execution and cancellation explicit. A forgotten future may therefore do nothing, often producing an unused_must_use warning.

Deep Dive

Laziness separates a computation's description from its execution. Combinators and .await can build one larger future, and an executor decides when and where to poll it.

This differs from eager promise models where creating a promise may immediately schedule work. In Rust:

  • .await drives a child future as part of the current task;
  • spawn registers a future as an independently scheduled task;
  • dropping an uncompleted future cancels its remaining computation.

Some ordinary code may run before a future is returned if it is outside the async block. The guarantee is not that every API returning a future has no side effects; it is that a future itself advances only through polling.

Internal Model

Constructing a generated future initializes its captured fields and initial state. The body runs when poll enters that state.

The #[must_use] annotation on futures helps catch accidental construction without awaiting or polling, but it cannot decide whether intentionally dropping a future is a bug.

Example

rust
use std::sync::{
    atomic::{AtomicBool, Ordering},
    Arc,
};

fn operation(started: Arc<AtomicBool>) -> impl std::future::Future<Output = ()> {
    async move {
        started.store(true, Ordering::SeqCst);
    }
}

#[tokio::main]
async fn main() {
    let started = Arc::new(AtomicBool::new(false));
    let future = operation(Arc::clone(&started));

    assert!(!started.load(Ordering::SeqCst));
    future.await;
    assert!(started.load(Ordering::SeqCst));
}

Common Mistakes

  • Assuming calling an async fn schedules it.
  • Creating a future, assigning it to _, and expecting its side effects.
  • Claiming an API cannot do any eager work before returning a future.

Follow-ups

  • What is the difference between awaiting and spawning a future?
  • How does dropping a lazy future implement cancellation?
  • Why does Rust not include a built-in global executor?

References

  • Rust Standard Library
  • Async Book