| |

Rust 80 🦀 Asynchronous Programming Concepts (Future Trait)

⏱️ Estimated read: 16 min | |

Rust’s Future trait is the foundation of asynchronous programming, and its design is shaped by a single constraint that distinguishes it from every other language’s async model: futures in Rust are lazy and inert. Calling an async fn does nothing. The function returns a Future that must be actively polled to make progress . This is not a quirk—it is the mechanism that allows Rust to avoid running async code unless it is needed, and it is the reason the compiler warns that an unused Future “does nothing unless you .await or poll them” .

The trait itself is minimal. It has one associated type, Output, and one method, poll. The poll method takes self as a Pin<&mut Self> and a &mut Context, and returns a Poll<Self::Output> . The Poll enum has two variants: Ready(value) when the computation is complete, and Pending when it is not. The Context carries a Waker, which is a handle the future uses to tell the executor “poll me again when I can make progress.” The Pin is the mechanism that prevents the future from moving in memory, which is necessary because the state machine that the compiler generates for an async block can contain references to itself.

Key point: A Future is an inert value that must be polled to make progress. poll returns Poll::Ready when the computation finishes or Poll::Pending when it is waiting, and the Waker in the Context is the handle the future uses to schedule its next poll. Pin guarantees the future does not move, which is required because the compiler-generated state machine can contain self-references. Rust has no built-in runtime; the Future trait is the interface, and a runtime like tokio provides the executor that polls.


Why the Future trait is designed this way

The laziness problem. A future is a description of a computation, not the computation itself. Creating a future does not start a thread, allocate a timer, or send a network request. Those side effects happen only when the future is polled . This is why async fn returns a future without running its body, and why ignoring the return value produces a warning rather than a silent execution. The laziness allows the runtime to decide when and where the future runs, and it allows the future to be composed, stored, and cancelled before it ever starts.

The polling problem. A blocking future would defeat the purpose of async. If poll blocked until the computation finished, a single slow future would block the executor thread, and no other future could make progress. The poll method is therefore non-blocking by contract: it must return quickly, and if it cannot make progress, it returns Pending and arranges for the Waker to be called when it can . The executor then moves on to other futures, and returns to this one only when the Waker fires.

The self-reference problem. An async block compiles to a state machine. Each await point becomes a variant of the state machine, and the data that lives across the await becomes a field of that variant. Some of that data can be a reference to another field of the same state machine . Moving the state machine after it has been polled would invalidate those internal references, because the references point to the old memory address. Pin prevents the move: Pin<&mut Self> is a pinned pointer, and the value behind it cannot be moved . The poll signature uses Pin for exactly this reason.

The wake problem. A future that returns Pending without arranging a wake will never be polled again. The task hangs silently. The Waker is the mechanism that prevents this: the future clones the Waker from the Context, stores it, and calls wake() when the underlying event occurs—a socket becomes readable, a timer fires, a thread finishes . The executor then re-polls the future. The Waker is the bridge between the future and the executor.


a. The poll method and the Context

The poll method is the only required method on the Future trait. Its signature is fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>. The Pin is the guarantee that self will not move. The Context provides the Waker.

// Import the types needed to implement Future
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};

// A future that is ready immediately
// This is the simplest possible Future implementation
struct ReadyFuture<T> {
    // The value to return when polled
    value: Option<T>,
}

impl<T> Future for ReadyFuture<T> {
    // The future produces a T
    type Output = T;

    // The poll method takes a pinned mutable reference and the context
    fn poll(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> {
        // Take the value out of the Option
        // The Option is None after this, so a second poll would panic
        // The Future contract says a completed future should not be polled again
        let value = self.value.take().expect("polled after completion");
        // Return the value as Ready
        Poll::Ready(value)
    }
}

The Pin<&mut Self> is the receiver. For a type that implements Unpin—which is most types, including ReadyFuture<T> if T is Unpin—the pinned reference can be obtained with Pin::new(&mut self), and the inner value can be accessed with Pin::get_mut(). For a type that does not implement Unpin, the value cannot be moved out of the Pin, and the future must use the safe APIs that Pin provides .

The Context is the handle to the executor. Its waker() method returns a &Waker, which can be cloned and stored. When the future can make progress, it calls waker.wake(), and the executor schedules the task for re-polling .


b. The Waker and the executor contract

The Waker is the future’s only way to tell the executor that it is ready to be polled again. The contract is precise: if poll returns Pending, the future must have arranged for the Waker to be woken at some point in the future. If it does not, the future will never be polled again, and the task will hang .

// A future that returns Pending once and then Ready
// It stores the Waker and calls wake after a delay
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;

struct DelayFuture {
    // The shared state between the future and the background thread
    state: Arc<Mutex<DelayState>>,
}

struct DelayState {
    // Whether the delay has elapsed
    done: bool,
    // The waker to call when the delay elapses
    waker: Option<std::task::Waker>,
}

impl Future for DelayFuture {
    type Output = ();

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
        // Lock the shared state
        let mut state = self.state.lock().unwrap();
        // If the delay has elapsed, return Ready
        if state.done {
            return Poll::Ready(());
        }
        // Store the waker so the background thread can call it
        // A clone is stored because the Context's waker is borrowed
        state.waker = Some(cx.waker().clone());
        // The future is not ready; return Pending
        Poll::Pending
    }
}

// The background thread that fires the wake
fn start_delay(state: Arc<Mutex<DelayState>>) {
    thread::spawn(move || {
        // Wait for the delay
        thread::sleep(Duration::from_millis(100));
        // Mark the delay as done
        let mut state = state.lock().unwrap();
        state.done = true;
        // Call the stored waker to schedule a re-poll
        if let Some(waker) = state.waker.take() {
            waker.wake();
        }
    });
}

The waker.wake() call is what moves the task from “waiting” to “ready to run.” The executor, which is the runtime’s scheduler, receives the wake and puts the task back on its ready queue. The next time the executor polls the future, state.done is true, and the future returns Poll::Ready .

The Waker is cloneable, and the future must store a clone because the Context is borrowed for the duration of the poll call. The Waker is also the mechanism that makes the future independent of the executor: the future does not know which executor is polling it, only that the Waker will schedule a re-poll.


c. Pin and Unpin

Pin is the guarantee that a value will not move. Unpin is the marker trait that says a type does not need that guarantee. The relationship is precise: Unpin is the normal case, and !Unpin is the special case that applies to the compiler-generated state machines of async blocks .

// Most types implement Unpin automatically
// A String does not need pinning because it has no self-references
// A Pin<&mut String> can be created and the String can be moved out of it
use std::pin::Pin;

fn unpin_example() {
    let mut s = String::from("hello");
    // Pin the String
    let pinned = Pin::new(&mut s);
    // The String is Unpin, so the inner value can be accessed
    let inner: &mut String = Pin::get_mut(pinned);
    // The String can be moved
    let moved = inner.clone();
}

// A type that is !Unpin cannot be moved out of a Pin
// The compiler-generated state machine of an async block is !Unpin
// This is why the Future trait's poll takes Pin<&mut Self>

The Pin type is not a pointer; it is a wrapper around a pointer that adds the guarantee. Pin<Box<T>> pins the T on the heap, and the Box pointer can move freely because the T does not move with it. Pin<&mut T> pins the T at its current location, and the reference cannot be used to move the T out .

The Unpin trait is implemented automatically for types that the compiler can prove are safe to move. String, Vec, and primitive types are all Unpin. The compiler-generated state machine of an async block is !Unpin because it can contain self-references. This is why the poll method requires Pin: it is the compiler’s way of ensuring that a future that needs to stay in place does stay in place, and that a future that does not need pinning is not burdened with the restriction .


Complete Example Session

The following session builds a hand-written Future that simulates an asynchronous operation, an executor that polls it, and demonstrates the Pending/Ready cycle with a Waker.

// ============================================
// PART 1: THE FUTURE
// ============================================
use std::future::Future;
use std::pin::Pin;
use std::sync::{Arc, Mutex};
use std::task::{Context, Poll, Waker};
use std::thread;
use std::time::Duration;

// The shared state between the future and the background thread
struct SharedState {
    // Whether the computation is complete
    done: bool,
    // The waker to call when the computation completes
    waker: Option<Waker>,
}

// A future that completes after a delay
struct AsyncDelay {
    // The shared state
    state: Arc<Mutex<SharedState>>,
}

impl Future for AsyncDelay {
    // The future produces nothing
    type Output = ();

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
        // Lock the shared state
        let mut state = self.state.lock().unwrap();
        // If the work is done, return Ready
        if state.done {
            return Poll::Ready(());
        }
        // Store the waker so the background thread can call it
        state.waker = Some(cx.waker().clone());
        // The future is not ready
        Poll::Pending
    }
}

// ============================================
// PART 2: THE CONSTRUCTOR
// ============================================
fn async_delay(millis: u64) -> AsyncDelay {
    // Create the shared state
    let state = Arc::new(Mutex::new(SharedState {
        done: false,
        waker: None,
    }));
    // Clone the state for the background thread
    let thread_state = Arc::clone(&state);
    // Spawn the background thread
    thread::spawn(move || {
        // Wait for the delay
        thread::sleep(Duration::from_millis(millis));
        // Mark the work as done
        let mut s = thread_state.lock().unwrap();
        s.done = true;
        // Call the waker to schedule a re-poll
        if let Some(waker) = s.waker.take() {
            waker.wake();
        }
    });
    // Return the future
    AsyncDelay { state }
}

// ============================================
// PART 3: THE EXECUTOR
// ============================================
// A simple executor that polls a future until it is ready
// It uses thread parking to sleep until the waker fires
fn block_on<F: Future>(future: F) -> F::Output {
    // Pin the future to the stack
    // The future will not move after this
    let mut future = Box::pin(future);

    // Create a waker that unparks the current thread
    // This is the simplest possible waker: waking means unparking
    let waker = futures::task::waker(Arc::new(ThreadWaker(thread::current())));
    // Create the context with the waker
    let mut cx = Context::from_waker(&waker);

    // Poll the future until it is ready
    loop {
        // Poll the pinned future
        match future.as_mut().poll(&mut cx) {
            // The future is done; return the value
            Poll::Ready(value) => return value,
            // The future is pending; park the thread
            // The waker will unpark it when the future can make progress
            Poll::Pending => thread::park(),
        }
    }
}

// The waker implementation
struct ThreadWaker(thread::Thread);

impl futures::task::Wake for ThreadWaker {
    fn wake(self: Arc<Self>) {
        // Unpark the thread, waking it from park()
        self.0.unpark();
    }
}

// ============================================
// PART 4: THE USAGE
// ============================================
fn main() {
    // Create the future (nothing happens yet)
    let future = async_delay(500);
    // Run the future to completion
    // The executor polls, parks, is woken, and polls again
    block_on(future);
    // This prints after the delay
    println!("Done!");
}

The four parts cover the future with its shared state, the constructor that spawns the background thread, the executor with its parking waker, and the usage. What the session demonstrates is that the future does nothing until polled, the first poll returns Pending and stores the waker, the background thread calls wake() after the delay, and the executor’s park() returns so it can poll again and receive Ready.


Quick Reference

The Future Trait

ItemValue
Associated typeOutput
Methodpoll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Output>
Ready variantPoll::Ready(value)
Pending variantPoll::Pending
Wake handleContext::waker()

The Poll Enum

VariantMeaning
Ready(T)Computation finished with value T
PendingComputation not ready; waker will be called

Pin and Unpin

TraitMeaning
Pin<P>Guarantee that the pointee will not move
UnpinMarker trait; the type is safe to move
!UnpinType needs pinning (async state machines)

Async/Await Desugaring

SyntaxEquivalent
async fn f() -> Tfn f() -> impl Future<Output = T>
async { ... }Anonymous Future implementation
expr.awaitpoll in a loop until Ready

Best Practices

✅ Do This:

// Implement poll with Pin
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> { ... }

// Store a clone of the waker
state.waker = Some(cx.waker().clone());

// Call wake when the future can make progress
waker.wake();

// Use Box::pin to pin a future on the heap
let mut future = Box::pin(future);

❌ Don’t Do This:

// Block inside poll
fn poll(...) { thread::sleep(...); } // blocks the executor

// Return Pending without arranging a wake
Poll::Pending // hangs forever if no wake is scheduled

// Move a pinned future
let pinned = Box::pin(future);
let moved = *pinned; // cannot move out of Pin

Common Pitfalls

PitfallWhy It HappensFix
Task hangsReturned Pending without a wakeStore the waker and call wake()
Blocking executorpoll blocksOffload blocking work to a thread
!Unpin compile errorTried to move a pinned futureUse Pin::get_mut or Box::pin
Polling after completionCalled poll after ReadyThe contract forbids it
No runtimeCalled .await in mainUse an executor like tokio

Real-World Examples

1. Async function desugaring

async fn foo() -> i32 { 42 }
// equivalent to: fn foo() -> impl Future<Output = i32>

2. Await a future

let value = foo().await;

3. Pin a future

let mut future = Box::pin(foo());

4. Poll a future

match future.as_mut().poll(&mut cx) {
    Poll::Ready(v) => v,
    Poll::Pending => { /* wait */ }
}

5. Store the waker

state.waker = Some(cx.waker().clone());

6. Wake the task

waker.wake();

7. Create a waker from a thread

let waker = futures::task::waker(Arc::new(ThreadWaker(thread::current())));

8. Park until woken

thread::park();

9. Unpark on wake

self.0.unpark();

10. Box::pin for storage

let futures: Vec<Pin<Box<dyn Future<Output = ()>>>> = vec![];

Visual

The Future Lifecycle

┌──────────────────────────────────────────────┐
│  Future created (nothing happens)            │
│         │                                    │
│         ▼                                    │
│  poll() called                               │
│         │                                    │
│    ┌────┴────┐                               │
│    │         │                               │
│  Ready    Pending                            │
│    │         │                               │
│    ▼         ▼                               │
│  return   store waker                        │
│  value    return                             │
│              │                               │
│              ▼                               │
│         event occurs                         │
│              │                               │
│              ▼                               │
│         waker.wake()                         │
│              │                               │
│              ▼                               │
│         poll() called again                  │
└──────────────────────────────────────────────┘

Pin and Self-Reference

┌──────────────────────────────────────────────┐
│  Async state machine:                        │
│  ┌────────────────────────────────────────┐  │
│  │ variant 1: data + reference to data    │  │
│  │ variant 2: data + reference to data    │  │
│  └────────────────────────────────────────┘  │
│         │                                    │
│         ▼                                    │
│  Moving invalidates internal references      │
│  Pin prevents the move                       │
└──────────────────────────────────────────────┘

Executor Loop

┌──────────────────────────────────────────────┐
│  loop {                                      │
│    poll(future)                              │
│    ├─ Ready → return                         │
│    └─ Pending → park()                       │
│  }                                           │
│                                              │
│  waker.wake() → unpark() → loop continues    │
└──────────────────────────────────────────────┘

Summary

ItemValue
TraitFuture
Output typetype Output
Methodpoll(Pin<&mut Self>, &mut Context) -> Poll<Output>
ReadyPoll::Ready(value)
PendingPoll::Pending
WakerContext::waker()
PinPrevents movement
UnpinMarker trait for movable types
RuntimeRequired to poll; not built-in

Key takeaways:

  • Futures are lazy and inert. An async fn returns a Future that does nothing until it is polled. The compiler warns if the returned future is ignored .
  • poll is non-blocking by contract. It returns Ready when the computation finishes, or Pending when it is waiting, and the future must arrange for the Waker to be called when it can make progress .
  • Pin exists because async state machines can self-reference. The compiler-generated state machine for an async block can contain references to its own fields. Moving it would invalidate those references, so poll takes Pin<&mut Self> .
  • Unpin is the normal case. Most types are safe to move, and the compiler implements Unpin automatically for them. The !Unpin case is the special case for self-referential types like async state machines .
  • The Waker is the bridge between the future and the executor. The future stores a clone of the waker and calls wake() when the underlying event occurs. The executor then re-polls the future .
  • Rust has no built-in runtime. The Future trait is the interface; a runtime like tokio or async-std provides the executor that polls and the reactor that fires the wakers .

Remember: The Future trait is Rust’s abstraction for asynchronous computation, and its design is a direct response to the constraints of the language. Futures are lazy because the runtime decides when work happens. poll is non-blocking because a blocking future would stall the executor. Pin exists because the state machine can contain self-references, and moving it would be unsound. The Waker exists because a future that returns Pending needs a way to say “I am ready now.” These are not arbitrary design choices—they are the minimum set of concepts that make async Rust work without a garbage collector, without a built-in runtime, and without sacrificing the safety guarantees that the language promises.


Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!