Why Rust’s Pin and Unpin Matter for Async Code
Writing async Rust libraries is where the language’s safety guarantees start to show their teeth. Using async/.await is straightforward, but implementing the Future trait by hand quickly surfaces types like Pin<&mut Self> and bounds such as T: ?Unpin. These exist for a specific reason: they make self-referential data structures safe in Rust’s ownership model.
The Future trait and the poll receiver
A Future is a type that can be polled. Each poll either returns Pending, meaning it should be polled again later, or Ready(value), meaning it has resolved. Implementing one manually is simple enough for a type that doesn’t hold another future internally.
The complication arrives when you try to wrap an existing future. Suppose you want a TimedWrapper that wraps an inner Future, records the start time on first poll, and then forwards subsequent polls to the inner future. A first attempt at the type definition and constructor is fine:
struct TimedWrapper<Fut: Future> {
start: Option<Instant>,
future: Fut,
}
The poll implementation is where it breaks down. Calling self.future.poll(cx) fails. The compiler’s error is instructive: the poll method is not defined on Fut directly, but on Pin<&mut Fut>. This is a different kind of receiver than self, &self, or &mut self, and it requires a pinned reference rather than a plain mutable one.
Why self-referential types are unsafe to move
The need for Pin comes from a fundamental problem with self-referential data types—structures that hold pointers to their own fields. These pointers are valid only as long as the struct stays at the same memory address. Rust can move values freely; when a struct is moved, its fields change addresses, but the pointers inside it still point to the old locations. These become dangling pointers, which are a source of undefined behaviour.
Most Rust types are safe to move. Primitive types, strings, and structs composed of them don’t contain self-referential pointers. Moving them doesn’t invalidate anything. These “movable” types implement the auto trait Unpin. Rust automatically determines this for most types, similar to how it implements Send and Sync.
Self-referential types are designated !Unpin (the ! notation means the trait is not implemented). For these types, a regular mutable reference isn’t enough—any move could be catastrophic. The Pin wrapper solves this by preventing the value from moving while it’s pinned. The only way to move a pinned value is if it implements Unpin, proving safety.
Projecting into a pinned struct
The poll method on Future takes a Pin<&mut Self> instead of a plain &mut self. So from our wrapper’s poll, we need to get a pinned reference to the inner future without moving it. The technique is called projection: given a pinned struct, you write helper methods that expose references to individual fields. Fields that need to be accessible in a pinned way require special handling.
Manually writing these projection methods is possible, but it involves unsafe code. While this unsafe block is justifiable, it’s often unnecessary. The pin-project crate generates these safe projections automatically. You mark fields that need pinned access with the #[pin] attribute:
#[pin_project]
struct TimedWrapper<Fut: Future> {
start: Option<Instant>,
#[pin]
future: Fut,
}
Fields without #[pin] get normal mutable reference projections, which is the simpler default. Fields marked with #[pin] provide pinned references, allowing you to call methods like poll that require Pin<&mut Self>. With this setup, polling the inner future becomes straightforward, and no unsafe appears in the application code.
A brief summary of Pin and Unpin
Rust’s type system classifies types into those safe to move and those that aren’t. The Unpin auto trait marks the former. For the latter, Pin provides a stricter pointer guarantee: the value behind it won’t be moved unless it proves safe to do so. Since many Future implementations hold self-referential pointers, the Future::poll method requires a pinned reference to guarantee memory safety during polling.
For most developers, the practical implication is that wrapping or implementing futures manually requires a bit of care. In those cases, reach for pin-project and let it handle the projections safely.



