Skip to main content

Futures

At the highest level:

A Future represents a value that will be available later.

But in Rust, that idea is made very explicit and low-level.

A future is:

  • lazy (nothing happens until it’s polled)
  • stateful (it remembers where it left off)
  • cooperatively scheduled (it must voluntarily yield)

The Future trait (real definition)

From std::future (simplified a bit):

use std::pin::Pin;
use std::task::{Context, Poll};

pub trait Future {
type Output;

fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}

This single method explains everything.

Understanding Poll

enum Poll<T> {
Ready(T),
Pending,
}
  • Ready(value) → the future is complete
  • Pending → not ready yet, poll me again later

A future is allowed to return Pending as many times as it wants.

Futures are lazy (very important)

This code does nothing:

async fn compute() -> i32 {
5
}

fn main() {
let fut = compute();
}

Why?

  • compute() returns a Future
  • That future is never polled
  • So it never runs

In Rust:

Creating a future does not start execution

Who polls a Future

A runtime / executor (Tokio, async-std, etc.).

The executor:

  1. Calls poll()
  2. If Pending, registers a waker
  3. Moves on to other futures
  4. When woken, polls again

What is Context and Waker

The Context gives the future a way to say:

“Hey executor, wake me up when I can make progress.”

Simplified:

struct Context<'a> {
waker: &'a Waker,
}

Waker

  • Stored by the future
  • Used to notify the executor
  • Called when external state changes (socket ready, timer fired, etc.)

A future is a state machine

Let’s build a tiny future by hand to see this clearly.

Example: a future that completes after being polled twice

use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};

struct TwoPollFuture {
polled_once: bool,
}

impl Future for TwoPollFuture {
type Output = &'static str;

fn poll(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> {
if self.polled_once {
Poll::Ready("done")
} else {
self.polled_once = true;
Poll::Pending
}
}
}

What’s happening

  • First poll → Pending
  • Second poll → Ready("done")
  • Internal state (polled_once) tracks progress

This is exactly what the compiler generates for async fn.

How async fn becomes a Future

async fn example() -> i32 {
let a = 1;
let b = async_call().await;
a + b
}

Roughly becomes:

enum ExampleState {
Start,
WaitingOnAsyncCall,
Done,
}

Each .await is:

  • A possible Pending return
  • A resume point

.await is just polling in a loop

Conceptually:

loop {
match future.poll(cx) {
Poll::Ready(val) => break val,
Poll::Pending => return Pending,
}
}

.await:

  • Polls the future
  • Suspends if Pending
  • Resumes at the same line later

Example: Custom future with a timer (Tokio)

use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use tokio::time::{Instant, Sleep};

struct MySleep {
sleep: Pin<Box<Sleep>>,
}

impl Future for MySleep {
type Output = ();

fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
self.get_mut().sleep.as_mut().poll(cx)
}
}

This wraps an existing future and delegates polling.

Using futures directly vs async/await

Using async/await (normal)

async fn work() -> i32 {
10
}

let result = work().await;

Using futures explicitly (rare, but educational)

use futures::executor::block_on;

let fut = work();
let result = block_on(fut);

Same thing. .await is just nicer syntax.

Futures are single-use

Once a future returns Ready:

  • It must never be polled again
  • Polling again is undefined behavior

That’s why futures are usually moved, not reused.

Why Pin<&mut Self>

Short version:

Futures must not move in memory once polling begins.

Because:

  • They may store self-references
  • .await points depend on stable addresses

Pin enforces this at compile time.

Send futures and multithreading

  • Future + Send → can move between threads
  • !Send futures → must stay on one thread

Tokio:

  • spawn requires Send
  • spawn_local does not

Key rules every future must follow

  1. Must not block
  2. Must return Pending when not ready
  3. Must wake the executor when progress is possible
  4. Must not be polled after completion
  5. Must not move once pinned

Break these and async breaks 😅