Asynchronous Rust
Futures
A future in Rust is a value that represents an asynchronous computation that may not have finished yet. Unlike callback-based promises in other languages (which allocate a heap object per step and push values eagerly through registered closures), Rust futures are poll-driven, zero-allocation state machines that sit inert until an executor pulls values from them via Future::poll.
Push-Based Promises (JavaScript / C#) vs. Pull-Based Futures (Rust)
| Architectural Property | Push-Based Promise (Promise<T> / Task<T>) |
Pull-Based Rust Future (impl Future<Output = T>) |
|---|---|---|
| Execution trigger | Eager (starts running immediately when constructed) | Lazy (does nothing until polled or .awaited) |
| Memory allocation | Heap-allocates a promise object and closure per .then() step |
Composes nested futures inline in a single struct (0 heap allocations per .await!) |
| Who drives progress? | Producer pushes result into registered continuation callback | Executor pulls by calling pinned_future.poll(cx) when notified by Waker |
| Cancellation cost | Requires wiring explicit AbortController / CancellationToken through every layer |
Zero-cost: dropping the Future immediately stops polling it and runs Drop on its active state! |
Composition of Nested Futures (`OuterFuture` awaiting `InnerFuture`):
In Heap-Based Languages (Pointer Chain of 3 Heap Allocations):
[ Outer Promise ] ---> [ Middle Promise ] ---> [ Leaf Socket Promise ]
In Rust (Single Contiguous State-Machine Struct — 0 Extra Heap Allocations!):
+-------------------------- OuterFuture --------------------------+
| local_id: u64 |
| +----------------------- MiddleFuture ----------------------+ |
| | buffer: [u8; 32] | |
| | +-------------------- LeafFuture ---------------------+ | |
| | | socket_fd: i32 | | |
| | +-----------------------------------------------------+ | |
| +-----------------------------------------------------------+ |
+-----------------------------------------------------------------+
Built-In Future Constructors and Adapters in std::future
The standard library (std::future) provides fundamental future building blocks out of the box without any external crate dependencies:
Function / Type in std::future |
Signature / Behavior |
|---|---|
ready(val: T) -> Ready<T> |
Returns a future that resolves to Poll::Ready(val) on its very first poll |
pending::<T>() -> Pending<T> |
Returns a future that never resolves (always returns Poll::Pending) |
poll_fn(f) -> PollFn<F> |
Wraps a closure FnMut(&mut Context<'_>) -> Poll<T> into a Future<Output = T> without writing a custom struct! |
IntoFuture |
Conversion trait invoked by .await |
Creating Custom Futures Instantly with std::future::poll_fn
Stabilized in Rust 1.64, std::future::poll_fn constructs a Future directly from a |cx: &mut Context<'_>| -> Poll<T> closure—making it easy to write custom poll logic inline inside an async fn:
use std::future::{Future, poll_fn};
use std::pin::pin;
use std::task::{Context, Poll, Waker};
async fn countdown(start: u32) -> &'static str {
let mut remaining = start;
// `poll_fn` turns a closure taking `&mut Context<'_>` directly into a Future:
poll_fn(|cx: &mut Context<'_>| {
if remaining == 0 {
println!(" countdown reached 0 -> Poll::Ready!");
Poll::Ready("liftoff!")
} else {
println!(" countdown at {} -> waking & returning Poll::Pending", remaining);
remaining -= 1;
cx.waker().wake_by_ref();
Poll::Pending
}
})
.await
}
fn main() {
let mut fut = pin!(countdown(2));
let mut cx = Context::from_waker(Waker::noop());
loop {
match fut.as_mut().poll(&mut cx) {
Poll::Pending => println!("executor: future returned Pending, polling again..."),
Poll::Ready(msg) => {
println!("executor: future completed with {:?}", msg);
break;
}
}
}
}
countdown at 2 -> waking & returning Poll::Pending
executor: future returned Pending, polling again...
countdown at 1 -> waking & returning Poll::Pending
executor: future returned Pending, polling again...
countdown reached 0 -> Poll::Ready!
executor: future completed with "liftoff!"