Future Trait
Future Trait
Level 10 — Async / Await The fundamental trait behind every async computation in Rust.
1. Prerequisites
async fn— The syntax that generates types implementingFuture.- Trait — The general concept of defining shared behavior in Rust.
pin!,Pin<T>, andUnpin— The memory guarantee that keeps futures from moving.
2. Term Category
Rust Core Interface (the state machine): Underneath the hood, every single async fn, async block, and timer in Rust is just a struct that implements a single core trait: std::future::Future.
A Future represents an asynchronous computation that may not have finished yet. It is physically a State Machine that is continuously polled by an executor until it returns a final result.
3. Explanation
(1) Design Motivation — "Why did we design this?"
In JavaScript, Promises are managed by the V8 C++ engine runtime. In Go, Goroutines are managed by a hidden Go runtime scheduler.
Rust wanted to be usable on bare-metal microcontrollers where no runtime exists! Therefore, Rust designed Future as a pure, zero-cost abstraction trait defined in core::future::Future.
It has a single method: poll().
- If the computation is done, it returns
Poll::Ready(output). - If the computation is still waiting (e.g., waiting for network I/O), it returns
Poll::Pendingand saves aWakernotification callback!
(2) Reality Metaphor
Imagine waiting for your order at a fast-food restaurant.
Future: Your receipt with an Order Number (e.g. Order #42). Holding the receipt doesn't mean you have the food yet; it represents future food.poll(): You walking up to the counter and asking: "Is Order #42 ready?"Poll::Pending: The clerk says "Not yet!" You hand the clerk your phone number (Waker) and go sit down.Poll::Ready(Burger): The clerk hands you the burger! The future is complete!
(3) Rust Code Examples
Short Snippet (The Trait Definition)
Here is the literal definition of the Future trait in Rust's standard library:
use std::pin::Pin;
use std::task::{Context, Poll};
pub trait Future {
// The type of the value produced when the future completes!
type Output;
// Driven forward by the Executor
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
Fuller Example (Building a Custom Future from Scratch)
Let's build a custom Future struct manually without using async fn! It will yield Poll::Pending twice, and then return Poll::Ready(100) on the 3rd attempt.
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
// Our custom state machine struct
struct CounterFuture {
count: u32,
}
impl Future for CounterFuture {
type Output = u32;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
self.count += 1;
if self.count >= 3 {
println!("Count reached 3! Returning Ready.");
Poll::Ready(100)
} else {
println!("Count is {}... Returning Pending.", self.count);
// Tell the executor to wake us up again immediately so we get polled again!
cx.waker().wake_by_ref();
Poll::Pending
}
}
}
#[tokio::main]
async fn main() {
let my_future = CounterFuture { count: 0 };
// We can .await our manually constructed Future state machine!
let result = my_future.await;
println!("Final Result: {}", result); // 100
}
4. Common Mistakes & Pitfalls
Mistake 1: Misunderstanding Future Trait Scoping and Lifecycle Rules
The mistake: Assuming Future Trait instances remain valid beyond their declaring scope block or across asynchronous boundaries without explicit lifetime tracking.
Why it's wrong: Rust strictly enforces lexical scope boundaries and non-lexical lifetimes (NLL) at compile time. Accessing dropped values or failing to handle variable drop order results in compiler errors such as E0597 or E0382.
Incorrect:
fn get_ref() -> &str {
let s = String::from("future_trait_data");
&s // ❌ Error E0106/E0515: returns a reference to data owned by the current function
}
Fix:
fn get_string() -> String {
let s = String::from("future_trait_data");
s // Ownership of the String is transferred directly to the caller
}
Mistake 2: Mutating Future Trait State Without Exclusive Ownership or mut Borrowing
The mistake: Attempting to mutate data associated with Future Trait through an immutable reference &T or without specifying mut in variable declarations.
Why it's wrong: Rust's aliasing XOR mutability rule (&T for shared immutable access, &mut T for exclusive mutable access) prohibits mutating state through shared references unless interior mutability patterns (e.g. RefCell, Mutex) are explicitly used.
Incorrect:
fn update_val(data: &i32) {
// *data += 1; // ❌ Error E0594: cannot assign to `*data`, which is behind a `&` reference
}
Fix:
fn update_val(data: &mut i32) {
*data += 1; // Correct: exclusive mutable reference permits mutation
}
Mistake 3: Concurrent Access to Future Trait Across Threads Without Send / Sync Guards
The mistake: Sharing non-thread-safe Future Trait instances across OS threads via std::thread::spawn.
Why it's wrong: Types that do not implement Send or Sync marker traits cannot safely cross thread boundaries. The compiler prevents data races by raising compile errors E0277 (trait Send is not implemented).
Incorrect:
use std::rc::Rc;
use std::thread;
let rc = Rc::new(42);
// thread::spawn(move || { println!("{}", rc); }); // ❌ Error E0277: `Rc` cannot be sent between threads safely
Fix:
use std::sync::Arc;
use std::thread;
let arc = Arc::new(42);
thread::spawn(move || {
println!("{}", arc); // Correct: `Arc` implements `Send` and `Sync`
});
5. Practice Exercises
Exercise 1: Polled Exponential Backoff Retry Future (RetryFuture)
Scenario: Low-level networking crates often implement custom Future state machines to handle retries without allocating extra futures on the heap. Implement a custom RetryFuture<F> struct that wraps a fallible closure/operation, tracks remaining attempts, registers wakers when pending, and executes backoff retries manually inside poll().
Requirements:
Build a manual Future implementation for retry logic.
Requirements:
- Implement
RetryFuture<F, T, E>holding state parametersmax_retries: usize,attempts: usize, and inner functionF: FnMut() -> Result<T, E>. - Implement
FuturereturningPoll::Ready(Ok(T))orPoll::Ready(Err(E))after exhausting retries. - In
poll, if the inner operation returnsErrandattempts < max_retries, incrementattempts, callcx.waker().wake_by_ref(), and returnPoll::Pending. - Add unit tests verifying instant success, success after attempts, and error return upon retry exhaustion.
Answer
Implementation
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
pub struct RetryFuture<F, T, E> {
op: F,
max_retries: usize,
attempts: usize,
}
impl<F, T, E> RetryFuture<F, T, E> {
pub fn new(max_retries: usize, op: F) -> Self {
Self {
op,
max_retries,
attempts: 0,
}
}
}
impl<F, T, E> Future for RetryFuture<F, T, E>
where
F: FnMut() -> Result<T, E> + Unpin,
{
type Output = Result<T, E>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
self.attempts += 1;
match (self.op)() {
Ok(val) => Poll::Ready(Ok(val)),
Err(err) => {
if self.attempts >= self.max_retries {
Poll::Ready(Err(err))
} else {
cx.waker().wake_by_ref();
Poll::Pending
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_retry_future_success() {
let mut count = 0;
let retry_fut = RetryFuture::new(3, move || {
count += 1;
if count == 2 {
Ok("SUCCESS")
} else {
Err("TRANSIENT_ERR")
}
});
let res = retry_fut.await;
assert_eq!(res, Ok("SUCCESS"));
}
#[tokio::test]
async fn test_retry_future_exhaustion() {
let retry_fut = RetryFuture::new(2, || Err::<(), _>("PERMANENT_ERR"));
let res = retry_fut.await;
assert_eq!(res, Err("PERMANENT_ERR"));
}
}
Technical Explanation
- Manual
FutureMechanics: ImplementingFuturedirectly requires definingtype Outputand writing thepoll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>state machine loop. - Waker Notification: When returning
Poll::Pending, callingcx.waker().wake_by_ref()informs Tokio's executor to reschedule the future for polling again.
Exercise 2: Shared Single-Execution Asynchronous Cell (AsyncOnceCell)
Scenario: Database connection pools or configuration loaders use an AsyncOnceCell<T> to ensure an expensive initialization future runs only once. If multiple caller tasks .await the cell concurrently while initialization is in progress, all callers register their Wakers and receive the initialized result once complete.
Construct a shared single-execution cell using manual Future waker registration.
Requirements:
- Define
AsyncOnceCell<T>with sharedArc<Mutex<CellState<T>>>. CellState<T>containsvalue: Option<T>,wakers: Vec<Waker>, andis_initializing: bool.- Implement
get_or_init<F>(&self, init: F)returning a customOnceCellFuture<T>. - Add unit tests asserting single initialization execution across multiple concurrent task callers.
[!check]- Answer
use std::future::Future;
use std::pin::Pin;
use std::sync::{Arc, Mutex};
use std::task::{Context, Poll, Waker};
struct CellState<T> {
value: Option<T>,
wakers: Vec<Waker>,
}
pub struct AsyncOnceCell<T> {
state: Arc<Mutex<CellState<T>>>,
}
impl<T: Clone> AsyncOnceCell<T> {
pub fn new() -> Self {
Self {
state: Arc::new(Mutex::new(CellState {
value: None,
wakers: Vec::new(),
})),
}
}
pub fn set(&self, val: T) {
let mut guard = self.state.lock().unwrap();
guard.value = Some(val);
for waker in guard.wakers.drain(..) {
waker.wake();
}
}
pub fn get(&self) -> OnceCellFuture<T> {
OnceCellFuture {
state: Arc::clone(&self.state),
}
}
}
pub struct OnceCellFuture<T> {
state: Arc<Mutex<CellState<T>>>,
}
impl<T: Clone> Future for OnceCellFuture<T> {
type Output = T;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let mut guard = self.state.lock().unwrap();
if let Some(ref val) = guard.value {
Poll::Ready(val.clone())
} else {
guard.wakers.push(cx.waker().clone());
Poll::Pending
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[tokio::test]
async fn test_async_once_cell_multi_waiter() {
let cell = Arc::new(AsyncOnceCell::<String>::new());
let cell1 = Arc::clone(&cell);
let cell2 = Arc::clone(&cell);
let h1 = tokio::spawn(async move { cell1.get().await });
let h2 = tokio::spawn(async move { cell2.get().await });
tokio::time::sleep(Duration::from_millis(10)).await;
cell.set("CONFIG_DATA".into());
assert_eq!(h1.await.unwrap(), "CONFIG_DATA");
assert_eq!(h2.await.unwrap(), "CONFIG_DATA");
}
}
Technical Explanation
- Multi-Waiter Waker Tracking:
CellStatemaintainswakers: Vec<Waker>. Concurrent callers pollingOnceCellFuturepush their wakers into the list when data is pending. - Fan-Out Waker Notification: Calling
cell.set(val)pops all stored wakers viaguard.wakers.drain(..)and invokes.wake()on each, unblocking all.awaiting tasks simultaneously.
Exercise 3: Custom Cancellation-Safe Select2 Combinator with Safe Pinned Projection
Scenario: tokio::select! races two futures. To understand its internal mechanics, build a custom zero-allocation Select2<F1, F2> combinator implementing Future.
Construct a manual Select2 combinator with pinned projection.
Requirements:
- Implement
Select2<F1, F2>struct holdingfut1: F1andfut2: F2. - Implement
FuturereturningPoll::Ready(Either<F1::Output, F2::Output>). - Perform structural pin projection using
unsafe { Pin::new_unchecked(...) }. - Add unit tests asserting branch winning behavior and cancellation of the losing future.
[!check]- Answer
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
#[derive(Debug, PartialEq, Eq)]
pub enum Either<A, B> {
Left(A),
Right(B),
}
pub struct Select2<F1, F2> {
fut1: F1,
fut2: F2,
}
impl<F1, F2> Select2<F1, F2> {
pub fn new(fut1: F1, fut2: F2) -> Self {
Self { fut1, fut2 }
}
}
impl<F1: Future, F2: Future> Future for Select2<F1, F2> {
type Output = Either<F1::Output, F2::Output>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
// SAFETY: Structural pin projection for fut1 and fut2
let (p1, p2) = unsafe {
let this = self.get_unchecked_mut();
(
Pin::new_unchecked(&mut this.fut1),
Pin::new_unchecked(&mut this.fut2),
)
};
if let Poll::Ready(out1) = p1.poll(cx) {
return Poll::Ready(Either::Left(out1));
}
if let Poll::Ready(out2) = p2.poll(cx) {
return Poll::Ready(Either::Right(out2));
}
Poll::Pending
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[tokio::test]
async fn test_select2_left_wins() {
let f1 = async {
tokio::time::sleep(Duration::from_millis(5)).await;
"LEFT_WIN"
};
let f2 = async {
tokio::time::sleep(Duration::from_millis(100)).await;
"RIGHT_WIN"
};
let res = Select2::new(f1, f2).await;
assert_eq!(res, Either::Left("LEFT_WIN"));
}
}
Technical Explanation
- Structural Pin Projection:
Select2projectsPin<&mut Select2<F1, F2>>intoPin<&mut F1>andPin<&mut F2>. - First-Completion Resolution: Polling both projected futures inside
pollreturnsPoll::Ready(Either::Left)orPoll::Ready(Either::Right)for whichever future completes first.
6. Related Terms
pin!,Pin<T>, andUnpin— Related concept:pin!,Pin<T>, andUnpin.- Generators / Coroutines (Unstable) — Related concept: Generators Coroutines.
WakerandContext— Related concept:WakerandContext.
7. Key Takeaways
- Every
asynccomputation in Rust is a struct implementingstd::future::Future.- A
Futureis a zero-cost State Machine — it contains no background threads or hidden runtime cost!- The core method is
poll(), which returnsPoll::Ready(value)orPoll::Pending.- It is lazy; it does nothing until polled by an Executor (like Tokio).
- When
Poll::Pendingis returned, the Future saves aWakercallback so Tokio knows when to wake it up again.