Why a Future That Points Into Itself Cannot Move After First Poll
Ordinary Rust structs cannot point into themselves. let s = String::from("hi"); let r = &s[..]; is fine, but move s and r dangles — the borrow checker exists precisely to stop that. Compiler-generated futures do it anyway, and understanding why is the whole topic.
async fn summarize(body: String) -> usize {
let head = &body[..64]; // reference INTO the state machine's own field
let score = rate(head).await; // `head` must stay valid across this await
score + head.len()
}
body and head are both live across the .await, so the lowering stores them as fields of the future itself, and head is a pointer to a sibling field. The moment the first poll runs, the future's address becomes part of its own state. Move it — mem::swap, a Vec reallocation, passing it by value — and head still points at the old location: use-after-free, in safe-looking code.
The standard library documents the lifecycle explicitly (std::pin):
| Phase | Address rule |
|---|---|
After calling the async fn, before first poll |
Freely movable — only arguments were moved in |
First poll |
The value starts depending on its address not changing |
Every later poll |
Internal unsafe code assumes the address is still the original |
| Drop | Must run before the memory is reused (the Drop guarantee) |
And it names our case outright: "A key example of such self-referential types are the state machines generated by the compiler to implement Future for async fns."
This is why the trait you have been calling since async fn and await looks odd:
pub trait Future {
type Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
self: Pin<&mut Self> — not &mut self — encodes the promise you may poll me, but you may not move me out of this slot while I am suspended. Safe code cannot manufacture that move either: Pin<&mut T> deliberately withholds DerefMut when T: !Unpin, so mem::swap(&mut *pinned, &mut other) fails to compile — you never get the &mut T needed to swap, replace, or take the value out.
A concrete picture helps. Before the first poll, the future is a struct of loose parts:
future { body: String, head: Option<&str>, score: u32 } <- movable: nothing points inward
|
first poll
v
future { body: String, head: &str --> points into `body`, score: u32 }
<- moving this struct now invalidates `head`
The historical detail interviewers enjoy: Pin shipped in Rust 1.33 (March 2019) and async/.await followed in 1.39 — pinning landed first, because without it the compiler's self-referential state machines could not be exposed through a safe poll.
Two clarifications interviews ask for:
Why not just heap-allocate every future? Box it and the address is stable — but composition nests futures several layers deep, and one allocation per layer (per combinator, per await chain) is exactly what Rust refused to pay. Pin is a type-level contract with zero runtime cost: no vtable, no flag, no check on poll.
Is it the compiler's magic? No. Pinning is a library contract between unsafe code and its users; the compiler does not track addresses. Safe code cannot violate it simply because every API that could move the value requires unsafe or an Unpin bound. Getting that boundary wrong is unsafe code's job — which is where The unsafe Keyword comes in.
Reading the Pin API: What the Wrapper Promises and What Unpin Gives Back
Pin<Ptr> wraps a pointer; it pins the pointee, never itself. Pin<Box<T>> can be moved, cloned into a variable, passed across threads — the box moves, the T inside does not. That distinction resolves most of the confusion.
let mut boxed: Pin<Box<Unmovable>> = Box::pin(Unmovable::new());
let mut local = Unmovable::new(); // still movable: not pinned yet
let pinned: Pin<&mut Unmovable> = std::pin::pin!(local); // stack pin (stable 1.68)
Unpin is the escape hatch: an auto-trait meaning this type is fine with moving, so Pin imposes nothing. When T: Unpin, Pin<Box<T>> behaves exactly like Box<T> and Pin<&mut T> like &mut T. Most types — String, Vec, Box<T>, references, plain structs — are Unpin, which is why pinning disappears from daily code until you touch futures.
| Value | Unpin? |
May the pointee move? |
|---|---|---|
String, Vec<T>, Box<T> |
yes | yes — no restrictions |
Pin<Box<T>> where T: !Unpin |
yes (the pointer is movable) | no |
| A self-referential async future, polled | no | no, until dropped |
A struct containing PhantomPinned |
no (opted out) | no |
Three ways to get a pin, and their costs:
let a: Pin<Box<F>> = Box::pin(fut); // heap: stable for the future's whole life
let b = std::pin::pin!(fut); // stack: pinned until the scope ends
let c = tokio::pin!(fut); // stack pin used inside tokio macros
Access rules follow from Deref/DerefMut:
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<u32> {
let this = self.get_mut(); // OK only because Countdown: Unpin
if this.remaining == 0 { return Poll::Ready(0); }
this.remaining -= 1;
Poll::Pending
}
as_mut()— reborrows asPin<&mut T>, cheap, used to poll repeatedly.get_mut()— yields&mut T, requiresT: Unpin; otherwise compile error: the trait bound is not satisfied forPin<&mut T>to implementDerefMut.get_unchecked_mut()/Pin::new_unchecked— the unsafe door, where you promise not to move the value. You will write these only when implementing address-sensitive types.into_inner()— consumes the pin, requiresUnpin, returns the value for normal use.
Making a normal type refuse to move takes three lines:
use std::marker::PhantomPinned;
struct Session {
id: u64,
_pin: PhantomPinned, // opts the type OUT of Unpin
}
With that field, Pin::new(&mut s) and mem::swap both stop compiling for Session, and any API promising stability can be trusted. One ergonomic concession survives: Pin::set(&mut pinned, new_value) is allowed — it runs the old value's destructor and then writes in place, so the address never changes and the Drop guarantee holds. Assigning through a pin is fine; moving out of one is not.
| Operation | Needs Unpin? |
|---|---|
pin.as_mut(), pin.as_ref() |
no — reborrow as a pin |
pin.get_mut() / DerefMut |
yes |
pin.set(value) |
no — drops in place, then writes |
pin.into_inner() |
yes — releases the value |
unsafe { pin.get_unchecked_mut() } |
no — you take responsibility |
One more invariant worth quoting: the Drop guarantee. Once a value is pinned, its memory must not be reused until its destructor has run — not just "not moved." That rules out sloppy patterns like pinning inside a ManuallyDrop (the destructor will never run, so the storage can be reused while pin-dependent code still trusts it), and it is why Vec::set_len-style tricks on pinned contents are unsound.
Self-check: if you can mem::replace the value out through your own API, pinning cannot be structural for it. The std docs call this structural pinning — whether a field is pinned when its parent is — and getting it wrong is the classic unsoundness in hand-written futures and combinators.
Where Pin Shows Up in Everyday Async Code
You will rarely write unsafe { Pin::new_unchecked(..) }. You will meet Pin in four ordinary places — recognize each on sight.
1. Manual polling. Testing a future or driving one inside a sync function:
let mut fut = Box::pin(load_config("app.toml"));
let mut cx = Context::from_waker(noop_waker_ref());
match fut.as_mut().poll(&mut cx) {
Poll::Ready(cfg) => println!("{cfg}"),
Poll::Pending => println!("needs an executor"),
}
Box::pin is required because poll demands Pin<&mut Self>, and Pin::new(&mut fut) only compiles when the future is Unpin. An async fn future is Unpin only if everything it carries across an .await is Unpin and none of it points back into the future — the instant it holds a self-borrow (like the head reference from the first chapter), it is !Unpin. Assume !Unpin until the compiler says otherwise.
2. Reusing a future in select!. Each loop iteration re-polls the same timer through &mut, and &mut F: Future holds only for F: Unpin:
let sleep = tokio::time::sleep(Duration::from_secs(5));
tokio::pin!(sleep); // makes `&mut sleep` pollable
loop {
tokio::select! {
_ = &mut sleep => break,
job = queue.recv() => process(job),
}
}
Tokio's select! documentation says it plainly: passing a future by reference requires it to be Unpin, achieved with Box::pin or stack pinning. Note the macro itself accepts non-Unpin futures when you hand it ownership — the pinning happens inside the macro-generated state; the &mut reuse pattern is what forces the bound.
3. Storing futures. A struct field, a Vec, or a channel of work items: Box<dyn Future<Output = T> + Send> is not enough (poll still needs Pin), so real code writes Pin<Box<dyn Future<Output = T> + Send>>, usually aliased:
type Task = Pin<Box<dyn Future<Output = io::Result<usize>> + Send>>;
let queue: Vec<Task> = jobs.map(|j| Box::pin(run(j)) as Task).collect();
4. The errors. Keep this table; each row is a real compiler answer:
| You wrote | Compiler says | Fix |
|---|---|---|
Pin::new(&mut fut) |
Unpin bound not satisfied |
Box::pin / pin! |
_ = &mut sleep in select! |
&mut F only implements Future when F: Unpin |
tokio::pin!(sleep) |
mem::swap(&mut *pinned, ..) |
cannot move out of a pinned value (DerefMut needs Unpin) |
don't move pinned data |
self.get_mut() in poll for !Unpin type |
Pin<&mut Self> does not implement DerefMut |
get_unchecked_mut() + safety comment |
Two habits make this concrete rather than theoretical. First, probe a type instead of guessing:
fn assert_unpin<T: Unpin>(_: &T) {}
assert_unpin(&std::future::ready(42)); // Ready<T>: plain struct -> Unpin
assert_unpin(&tokio::time::sleep(Duration::from_secs(1))); // compile error: Sleep: !Unpin
The compiler answers the Unpin question instantly — no runtime check exists to ask — and for an async fn future the answer is conditional: all-Unpin state with no self-borrow usually passes, a self-borrow across an await does not. Second, remember who pins on your behalf: tokio::spawn moves the future into the task's heap allocation already pinned, and macros like join!, select!, and timeout pin their operands inside the state they build. The requirement surfaces only when you keep the future: polling by hand, &mut reuse, or storing it — where the conventional type is Pin<Box<dyn Future<Output = T> + Send>> (the BoxFuture alias from the futures crate), created with exactly one call: Box::pin.
The interview answer in one paragraph: futures are self-referential state machines, so after the first poll they must not move; Pin encodes that promise in the type with no runtime cost, Unpin opts ordinary types out of the restriction, and Box::pin/pin! are how you satisfy poll's Pin<&mut Self> receiver. Practical consequence: tokio::spawn pins internally for you, but anything you keep — manual polls, &mut reuse, stored futures — needs explicit pinning.
For the unsafe half of this contract — new_unchecked, get_unchecked_mut, and the safety comments they demand — continue to The unsafe Keyword. For the cancellation story that made dropped futures worth reasoning about, revisit Timeouts, Cancellation, and Async I/O.
Quiz
1. When does an async fn's future become address-sensitive?
2. You have `let p: Pin<Box<Unmovable>>` where Unmovable: !Unpin. Which statement is correct?
3. Inside a hand-written `poll(self: Pin<&mut Self>, ..)` you call `self.get_mut()` on a type that is !Unpin. What happens?
4. Why must `&mut sleep` work across loop iterations of a select! while a raw async block passed by reference does not?
Flashcards
Question
Why does a polled future need to stay at a fixed address?
Click to reveal answer
Answer
Because it can be self-referential: locals live across an await are stored as fields and references into those fields are stored as values, so after the first poll a move would leave those pointers dangling. std::pin calls such values address-sensitive, and names compiler-generated Future state machines as the key example.
Question
What exactly does Pin guarantee, and what does it pin — the wrapper or the pointee?
Click to reveal answer
Answer
From the moment a Pinning pointer is created, the pointee stays valid at that same address until its drop runs (the Drop guarantee). It pins the pointee: Pin<Box<T>> can be moved freely because moving the box does not move the T; only operations that would move the T are restricted.
Question
What is Unpin, which types have it, and how do you opt out?
Click to reveal answer
Answer
Unpin is an auto-trait meaning the type has no address-sensitive state, so Pin imposes no restriction — primitives, String, Vec, Box<T>, references and most structs have it. Opt out by adding a PhantomPinned field (or !Unpin inner types); then Pin's rules apply and you need unsafe to move the value.
Question
Is an async fn future Unpin, and when does it matter in practice?
Click to reveal answer
Answer
Only if everything it carries across awaits is Unpin and nothing points back into the future — a self-borrow across an await makes it !Unpin. It matters whenever YOU keep the future: manual poll (needs Pin<&mut Self>), reusing it via &mut in select!, or storing it; tokio::spawn and join! take ownership and pin internally.
Question
Compare Box::pin and std::pin::pin! for making a future pollable.
Click to reveal answer
Answer
Box::pin allocates on the heap and returns Pin<Box<F>> — stable for the future's whole life and storable/'static, at the cost of an allocation. pin! (and tokio::pin!) pins a value in place on the stack, returning Pin<&mut F> valid until the scope ends — zero allocation, but the future cannot leave the scope. Both satisfy poll's Pin<&mut Self> receiver.
Revision Notes
Key Takeaways
- 1. A future is freely movable until its first poll; from then on its state may point into itself, so the address must not change
- 2. Pin is a zero-cost library contract on the pointee — no compiler magic, no runtime check — and the Drop guarantee covers the memory too
- 3. Unpin cancels every restriction; most types are Unpin, and PhantomPinned is how a type opts out
- 4. Box::pin buys a permanent heap address, pin! buys a stack address valid to end of scope; both satisfy poll's Pin<&mut Self>
- 5. Everyday symptoms are compile errors: Pin::new, &mut reuse in select!, and get_mut in poll all fail their Unpin bound on !Unpin futures
Interview Tips
- • "Why does Future::poll take Pin<&mut Self>?" — async fn state machines are self-referential; after first poll a move dangles; Pin encodes don't-move-me in the type at zero cost
- • "What is Unpin and why do most people never see it?" — auto-trait meaning moving is harmless; almost every everyday type has it, so Pin is transparent until you hold a self-borrowing future across an await
- • "Box::pin or stack pin?" — heap: stable, storable, 'static, one allocation; stack: free but scoped to the block; mention Pin<Box<T>> stays movable while the pointee does not
- • "Give an example of code that fails without pinning" — &mut fut in select! (impl Future for &mut F needs Unpin) or self.get_mut() in a manual poll (DerefMut needs Unpin), then name the fix: tokio::pin! / Box::pin / get_unchecked_mut with a safety comment
Cheat Sheet
Pin and Unpin — Cheat Sheet
The problem: an async fn's state machine can hold references into its own fields; after the first poll, moving it dangles. poll(self: Pin<&mut Self>) forbids the move at the type level.
Lifecycle: movable until first poll → address fixed while polled → memory reused only after drop runs (Drop guarantee).
Core types:
let b: Pin<Box<F>> = Box::pin(fut); // heap pin, stable for the future's life
let s = std::pin::pin!(fut); // stack pin (1.68+), lives to end of scope
let t = tokio::pin!(fut); // stack pin used by tokio macros
pin.as_mut().poll(cx); // reborrow and poll repeatedly
pin.get_mut() // &mut T — requires T: Unpin
pin.get_unchecked_mut() // unsafe: you promise no move
Unpin: auto-trait meaning "moving me is always fine." Implemented by String, Vec, Box<T>, references. Opting out needs PhantomPinned. Pin<Box<T>> itself is Unpin — the pointer may move, the pointee may not.
| Situation | Unpin? |
|---|---|
| plain values, combinators over them | yes |
async fn future with only Unpin parts, no self-borrow |
yes |
async fn future holding a self-reference across an await |
no |
tokio::time::Sleep, custom Future with PhantomPinned |
no |
Error → fix:
`Unpin` bound not satisfied (Pin::new / &mut fut in select!) -> Box::pin or pin!
cannot move out of a pinned value (mem::swap through Pin) -> never move pinned data
Pin<&mut Self> does not implement DerefMut (get_mut in poll) -> get_unchecked_mut + SAFETY