How to Implement the Future Trait from Scratch

You cannot implement the `Future` trait from scratch in user code because it is sealed and requires unsafe compiler internals to define the `poll` method correctly. Instead, you must use the `async` keyword to create a future or wrap existing asynchronous logic in a library-provided future type like

The wall you hit when you try to build a Future

You type impl Future for MyTask and the compiler immediately demands a poll method that takes a Pin<&mut Self> and a &mut Context. You spend an hour wiring it up, run it, and the program hangs. The problem is not your logic. The Future trait is not a normal trait like Display or Clone. It is a contract with an executor, and the contract requires you to manage your own state machine, guarantee memory stability, and coordinate with a waker system. Most developers never write this contract by hand. They let the compiler write it.

What a Future actually is

A Future is a value that says, "I am not finished, but I can make progress if you ask me." Think of it like a ticket at a busy kitchen. You hand the ticket to the chef. The chef checks it every few seconds. If the ingredients are ready, they cook the dish and hand it back. If not, they put the ticket down and leave a note telling you when to check again. The poll method is that check. The Waker is the note.

Rust does not use threads to pause and resume code. It uses cooperative scheduling. When an async operation blocks, it yields control back to the executor. The executor runs other tasks. When the I/O is ready or the timer expires, the executor calls poll again. The trait signature looks intimidating because it has to expose the exact mechanics of this handoff.

use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};

/// A manual future that counts to three, then returns a result.
pub struct CountFuture {
    /// Tracks how many times we have been polled.
    count: u32,
}

impl Future for CountFuture {
    type Output = String;

    /// The executor calls this method to ask for progress.
    fn poll(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> {
        // Bump the counter each time we are polled.
        self.count += 1;

        // Return Ready when the work is finished.
        if self.count >= 3 {
            Poll::Ready("Done".to_string())
        } else {
            // Return Pending to tell the executor we need more time.
            Poll::Pending
        }
    }
}

This compiles. It also deadlocks if you try to run it with a real executor. The executor will call poll, get Pending, and never call it again because we never told it when to wake us up. The trait gives you the skeleton. You have to supply the nervous system.

The compiler does the heavy lifting

When you write an async block, the compiler rewrites it into a zero-cost state machine. It tracks every variable, every drop, and every suspension point. It generates the poll method for you. It handles the Pin boilerplate. It registers wakers automatically when you await another future.

use std::future::Future;

fn main() {
    // The compiler turns this into a state machine struct.
    let my_future = async {
        // Suspension point 1: wait for something
        println!("Step one");
        
        // Suspension point 2: wait for something else
        println!("Step two");
        
        42
    };

    // The future exists but does nothing until polled.
    let _ = my_future;
}

The async keyword is syntactic sugar for a very precise transformation. The compiler creates an enum where each variant represents a state between suspension points. It moves variables into the struct. It generates Drop implementations for the right scope. It writes the poll method that matches on the current state, advances to the next state, and returns Poll::Ready when the block finishes. You get all of this without touching Pin or Context.

Trust the compiler here. Writing state machines by hand is how bugs are born.

Building one by hand (and why it hurts)

You only implement Future manually when you are building a runtime, a custom I/O abstraction, or a highly optimized combinator. Even then, you usually wrap existing futures instead of starting from zero. The pain comes from three places: state tracking, pinning, and waker registration.

State tracking requires you to decide what lives across poll calls. If you allocate a Vec in the first poll, you must store it in the struct so it survives the second poll. If you forget, you get E0382 (use of moved value) when the compiler tries to move a temporary out of the poll method. You have to manually manage ownership across suspension points.

Pinning guarantees that the future never moves in memory after the first poll. Futures often store self-references or pointers to their own fields. If the value moves, those pointers dangle. Pin prevents accidental moves. You must use Pin::as_mut() or Pin::new_unchecked carefully. The community convention is to keep Pin usage strictly at the boundary of your poll implementation. Never reach for unsafe to unpin a value unless you have proved it contains no self-references.

Waker registration is the silent killer. If you return Poll::Pending without calling cx.waker().wake_by_ref(), the executor assumes you will never be ready. Your task sleeps forever. If you call wake_by_ref() but forget to store the waker or check a condition, you get a thundering herd of polls that waste CPU cycles.

use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll, Waker};

/// A future that waits for an external signal before resolving.
pub struct WaitFuture {
    /// Tracks whether the external signal has arrived.
    signaled: bool,
    /// Stores the waker so we can wake ourselves later.
    waker: Option<Waker>,
}

impl Future for WaitFuture {
    type Output = ();

    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        // Check if the condition is already met.
        if self.signaled {
            return Poll::Ready(());
        }

        // Register the waker so the executor knows how to resume us.
        if let Some(ref mut stored_waker) = self.waker {
            // Clone the waker if the executor provided a new one.
            stored_waker.clone_from(cx.waker());
        } else {
            self.waker = Some(cx.waker().clone());
        }

        // Return Pending to yield control back to the executor.
        Poll::Pending
    }
}

impl WaitFuture {
    /// External code calls this to signal completion.
    pub fn signal(&mut self) {
        self.signaled = true;
        // Wake the executor so it polls us again.
        if let Some(waker) = self.waker.take() {
            waker.wake();
        }
    }
}

This pattern shows the exact mechanics. You store the waker. You clone it on each poll. You return Pending until an external event flips the flag. When the flag flips, you call wake(). The executor sees the waker, schedules the task, and calls poll again. The next poll sees signaled is true and returns Ready.

Treat the waker as a phone number. If you lose it, nobody can call you back.

How the polling loop works

Executors run a loop that looks roughly like this:

  1. Take a future from the ready queue.
  2. Create a Context with a waker tied to that future.
  3. Call future.poll(&mut context).
  4. If Ready, run the continuation. If Pending, leave it alone until the waker fires.

The waker is just a trait object with a wake method. When you call wake(), it pushes the future's task ID back onto the executor's queue. The executor picks it up in the next cycle. This is why async Rust is fast. There is no thread parking. No kernel context switches. Just a queue and a loop.

The Pin requirement exists because some futures store pointers to their own fields. A self-referential struct breaks if it moves. Pin makes the compiler reject any code that would move the value after it is pinned. You get safety without runtime overhead. The compiler enforces it at the type level.

Convention aside: always use Pin::as_mut() when calling poll on a boxed future. Box::pin() creates the pinned box. pin.as_mut() gives you the Pin<&mut T> the trait expects. Skipping the as_mut() call triggers E0277 (trait bound not satisfied) because Box does not implement Future directly.

Where things go wrong

Manual futures fail in predictable ways. The compiler catches most of them early.

If you try to move a pinned future, the compiler rejects it with a hard error. You cannot call std::mem::replace on a Pin<&mut T> unless you use Pin::map_unchecked_mut, which requires unsafe. The // SAFETY: comment must prove the type contains no self-references. If you cannot write that proof, do not use it.

If you forget to register a waker before returning Pending, your task deadlocks. The executor has no way to resume it. You will see no compiler error. You will just watch CPU usage drop to zero and wonder why your program stopped.

If you store a &mut reference across a suspension point, the borrow checker rejects it with E0502 (cannot borrow as mutable because it is also borrowed as immutable) or a lifetime error. Futures cannot hold mutable borrows that outlive a single poll call. Use RefCell or interior mutability if you need to mutate shared state, or restructure the future to own its data.

If you return Ready but the executor expects Pending, you get a panic or undefined behavior depending on the runtime. Most executors assume Ready means the future is done. Calling poll again on a Ready future is undefined behavior. The compiler cannot check this. You have to track state yourself.

Counter-intuitive but true: the more you hand-roll futures, the harder the rest of your code becomes to reason about. Stick to combinators unless you are building the plumbing.

Which tool fits your problem

Use async blocks when you are writing application logic and need to chain multiple non-blocking operations. The compiler generates the state machine, handles pinning, and registers wakers automatically. You get readable code with zero overhead.

Use std::future::ready or std::future::pending when you need a trivial future for testing, stubbing, or returning immediate values from a trait that expects a Future. They allocate nothing and require zero setup.

Use manual impl Future when you are building a runtime, a custom I/O abstraction, or a zero-allocation state machine where the compiler's generated machine adds unacceptable overhead. You must track state, manage wakers, and prove pinning safety yourself.

Reach for Pin only at the boundary of your poll implementation. Keep the pinned region small. Never unpin a value unless you have verified it contains no self-references.

Where to go next