Pin and Self-Referential Types

Pin is the piece of Rust most people meet only when a confusing async error demands it, the trait `Unpin` is not implemented, or "cannot be unpinned", and then copy a Box::pin incantation without understanding why. This tutorial demystifies it. Pin exists to solve one specific problem: some types (notably the futures behind async/await from Async/Await with Tokio) hold internal references to their own data, and moving such a value in memory would invalidate those references. Pin is the type-system tool that promises "this value will not move," making self-referential types sound. You rarely write Pin by hand, but understanding it turns those async errors from mysterious to obvious.


The Problem: Moving Invalidates Internal Pointers

Rust moves values freely, a move is just a bitwise copy to a new location, and it's normally safe because Rust values don't contain pointers to themselves. But some types do. Consider a struct that holds both some data and a pointer into that same data:

struct SelfRef {
    data: String,
    // a pointer into `data` above — points at data's buffer
    pointer: *const u8,
}
// If a SelfRef is MOVED, `data` relocates but `pointer` still holds the OLD address —
// now it dangles, pointing at freed/garbage memory.

The moment such a value moves, its self-pointer becomes wrong: data moved to a new address, but pointer still holds the old one. This is exactly the situation async blocks create, an .await that borrows a local across the await point compiles into a future that references its own fields. Normal Rust would let that future be moved, breaking the internal reference. Something has to forbid the move.


Pin: A Promise Not to Move

Pin<P> is a wrapper around a pointer type P (like Pin<Box<T>> or Pin<&mut T>) that makes one guarantee: the value it points to will never move again until it's dropped. It doesn't move anything itself, it removes the ability to move the value, by not handing out a &mut T that could be used with mem::swap/mem::replace to relocate it.

use std::pin::Pin;

// pin a value on the heap — it now has a stable address for its whole life
let mut pinned: Pin<Box<SelfRef>> = Box::pin(build_self_ref());

// you can use &pinned, but you CANNOT get a &mut SelfRef out to move/swap it
// → the self-pointer stays valid because the value can't relocate

The key insight: Pin doesn't make a value special, it withholds the capability (safe &mut access) that would let you move it. Once a value is behind Pin<Box<T>>, safe code can no longer std::mem::swap it or otherwise relocate it, so any internal self-references remain valid. That's the entire purpose, a compile-time promise of address stability.


Unpin: Why Most Types Ignore Pin Entirely

If Pin restricts moving, why can you still move Strings and i32s freely? Because of the Unpin auto trait. Unpin means "this type is not self-referential, so pinning it does nothing", it's safe to move even when pinned. Almost every type is Unpin (auto-derived, like Send/Sync from Send and Sync Deep Dive), so Pin has no effect on them.

// i32, String, Vec<T>, your normal structs: all Unpin — Pin is a no-op for them
fn takes_pinned<T: Unpin>(_: Pin<&mut T>) { /* can freely move T; Pin adds no restriction */ }

The only types that are !Unpin (genuinely restricted by Pin) are self-referential ones, chiefly the anonymous futures the compiler generates for async fn/async {} blocks. So the mental model inverts nicely: Pin restricts nothing for ordinary types, and clamps down only on the handful that actually need address stability. When you see Unpin in an error, the compiler is really saying "this value might be self-referential, so I won't let you move it out of its pin."

Gotcha: the confusing async errors, `dyn Future` cannot be unpinned, "must be pinned before polling", almost always mean you need to pin a future before using it, not that something is deeply wrong. The fix is usually Box::pin(fut) (heap-pin, the easy default) or the tokio::pin!/std::pin::pin! macro (stack-pin, no allocation). Future::poll takes Pin<&mut Self> precisely because a future may be self-referential and must not move between polls, so anything that drives a future by hand (a manual executor, select! over stored futures, streams) has to pin it first. You're not doing something exotic; you're satisfying the "won't move" promise poll requires.


Where Pin Actually Shows Up

You almost never design a Pin-based API, that's rare, low-level work. In practice Pin appears in a few specific places, and knowing them is enough:

// 1. pinning a future to poll/await it manually or store it
let fut = async { compute().await };
let mut fut = Box::pin(fut);           // Pin<Box<dyn Future>>
// now fut can be polled

// 2. holding a future in a struct (e.g. a custom Stream/state machine)
struct MyTask {
    inner: Pin<Box<dyn std::future::Future<Output = ()> + Send>>,
}

// 3. the pin! macro for stack pinning without heap allocation
let fut = async { /* ... */ };
let mut fut = std::pin::pin!(fut);     // pinned on the stack

The recurring theme: Pin shows up when you store or manually drive a future rather than just .await-ing it inline. Plain async/.await code hides all of this, the compiler pins futures for you behind the scenes. You only reach for Box::pin/pin! when you step outside that (storing futures in a struct, hand-writing a Stream, building an executor). For everyday async, Pin is invisible; for those specific cases, Box::pin is almost always the right answer.


Pin Quick Reference

SituationWhat to know / do
Ordinary type (String, your struct)it's Unpin; Pin does nothing, move freely
An async fn/async {} futureit's !Unpin; must be pinned before polling
"cannot be unpinned" / "must be pinned" errorpin the future: Box::pin(fut)
Pin a future on the heapBox::pin(fut)Pin<Box<_>> (easy default)
Pin a future on the stackstd::pin::pin! / tokio::pin! (no allocation)
Store a future in a structfield type Pin<Box<dyn Future<...>>>
Just writing async/.await inlinenothing, the compiler pins for you

The mental model: Pin is a promise that a value won't move, needed only by self-referential types, which in practice means compiler-generated futures. Unpin (nearly every type) opts out entirely, so Pin is a no-op for normal code. When an async error mentions pinning or Unpin, reach for Box::pin and move on, you've satisfied a real safety requirement, not stumbled into something broken.


Key Takeaways

  • Pin exists to keep self-referential values from moving: a value holding a pointer into its own data would dangle if relocated, and moving is normally unrestricted, so something must forbid it.
  • Pin<P> (e.g. Pin<Box<T>>) guarantees the pointed-to value never moves again by withholding safe &mut access, it doesn't move anything, it removes the ability to.
  • Unpin (an auto trait almost every type has) means "not self-referential," so Pin is a no-op; only !Unpin types, chiefly the futures generated by async, are actually restricted.
  • Confusing async errors about "unpinned"/"must be pinned" almost always just mean pin the future first: Box::pin(fut) (heap, the easy default) or std::pin::pin!/tokio::pin! (stack). Future::poll requires Pin<&mut Self> because futures mustn't move between polls.
  • You rarely write Pin yourself; it appears when you store or manually drive futures (custom streams, executors, futures in structs). Plain inline async/.await pins everything for you.