TL;DR: A
Futureis a Rust trait representing a computation that hasn't completed yet. Futures are lazy; they do nothing until polled by an executor. Everyasync fnreturns aFuture. Calling.awaittells 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.awaitpoint.
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
- std::future::Future: The core trait definition
- The Rust Async Book; Under the Hood
- Tokio Tutorial; Async in depth
Related Glossary Terms
- Async/Await: The syntax that creates and drives futures
- Tokio: The most common executor that polls futures
- Trait:
Futureis a trait;async fnimplements it automatically - Pin: Required by
Future::pollto prevent self-referential moves - select:
select!works by polling multiple futures and taking the first ready branch
Keep Reading
- Rust Ownership and Borrowing Explained: Futures are owned values passed between async tasks
- Rust on AWS Lambda: async handlers based on Future in serverless Rust
- Building AI Agents in Rust: composing Futures to build concurrent AI workflows
