Future in Rust: Polling & Combining Futures Explained

Max WellsMax WellsFounder of Rustify

TL;DR: A Future is a Rust trait representing a computation that hasn't completed yet. Futures are lazy; they do nothing until polled by an executor. Every async fn returns a Future. Calling .await tells the executor to poll the future and suspend the current task until it's ready. Rust futures compile to zero-overhead state machines with no heap allocation per .await point.


What Is a Future in Rust?

A Future is a trait that represents an asynchronous computation; a value that will be produced at some point in the future without blocking the current thread.

pub trait Future {
    type Output;
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
 
pub enum Poll<T> {
    Ready(T),    // computation is done, here's the result
    Pending,     // not done yet, will wake up the executor later
}

Every async fn is syntactic sugar that returns impl Future<Output = T>. You rarely implement Future by hand; the compiler generates the state machine for you.


How Does Polling Work?

An executor drives futures by calling poll() repeatedly. If poll() returns Pending, the future registers a Waker (a callback that notifies the executor when it's ready to make progress).

use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::time::{Duration, Instant};
 
// A simple future that resolves after a delay
struct Delay {
    deadline: Instant,
}
 
impl Future for Delay {
    type Output = ();
 
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
        if Instant::now() >= self.deadline {
            Poll::Ready(())
        } else {
            // Tell the executor to wake us up later
            cx.waker().wake_by_ref();
            Poll::Pending
        }
    }
}

In practice, you almost never write this; Tokio's timer primitives handle the waker registration efficiently.


What Is the Difference Between a Future and async fn?

async fn is syntax sugar for a function that returns impl Future. The compiler transforms the function body into a state machine implementing the Future trait.

// These two are equivalent:
 
async fn fetch(url: &str) -> String {
    reqwest::get(url).await.unwrap().text().await.unwrap()
}
 
// What the compiler roughly generates:
fn fetch(url: &str) -> impl Future<Output = String> {
    async move {
        reqwest::get(url).await.unwrap().text().await.unwrap()
    }
}

Each .await point is a state transition in the generated state machine. The future suspends at that point and only resumes when the awaited future completes.


How Do You Combine Multiple Futures?

Use tokio::join! to run futures concurrently within one task, tokio::select! to race them, or tokio::spawn to run them as independent tasks.

use tokio::time::{sleep, Duration};
 
#[tokio::main]
async fn main() {
    // join!; run both concurrently, wait for both
    let (a, b) = tokio::join!(
        fetch_data("endpoint-a"),
        fetch_data("endpoint-b"),
    );
 
    // select!; use whichever completes first
    tokio::select! {
        result = fetch_data("endpoint-a") => {
            println!("A finished first: {:?}", result);
        }
        result = fetch_data("endpoint-b") => {
            println!("B finished first: {:?}", result);
        }
    }
}

join! is for "I need all results." select! is for "I need the first result" or timeout patterns.


Why Are Futures Lazy?

A Future does nothing until something polls it; creating a future has zero side effects. This enables zero-cost composition: you can build complex chains of futures without any work happening until an executor drives them.

// This does NOT start the HTTP request; it just builds a Future value:
let future = reqwest::get("https://example.com");
 
// The request only starts when we await it:
let response = future.await?;

This laziness is different from Node.js Promises, which start executing immediately when created. In Rust, you're always in control of when a future runs.


Frequently Asked Questions

Rarely. async fn and async {} blocks cover almost all use cases. Manual Future implementations are needed for low-level constructs like custom timers, I/O primitives, or wrapping callback-based APIs.

Pin<&mut Self> prevents a future from being moved in memory after it's been polled. This matters because futures often contain self-referential data (references into themselves). Pinning guarantees the future stays at a stable memory address.

Future produces exactly one value (like a single async computation). Stream is the async equivalent of Iterator; it produces zero or more values over time. Tokio and the futures crate provide StreamExt adapters for streams.

Nothing; the future is dropped and its computation never runs. The compiler will warn you with "unused impl Future that must be used."


Sources


  • Async/Await: The syntax that creates and drives futures
  • Tokio: The most common executor that polls futures
  • Trait: Future is a trait; async fn implements it automatically
  • Pin: Required by Future::poll to prevent self-referential moves
  • select: select! works by polling multiple futures and taking the first ready branch

Keep Reading

Ready to Land a $80-120k Rust Job?