Pin & Unpin
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Rust 1.99.0, the 2024 edition and pin-project-lite 0.2, checked on 2026-10-06. Pin is the
part of async Rust that most often surfaces as a confusing error — a type that cannot be unpinned, or a missing Unpin implementation. It exists to solve one problem: some values — including most futures produced by async — must not be
moved in memory once they have started running. This page explains why, and shows the handful of patterns you need in
practice.
The problem: self-referential values
Moving a value in Rust is a bitwise copy to a new address. That is fine for almost every type, but not for one that contains a pointer into itself: after the move, the pointer still targets the old address. Safe Rust will not let you build such a struct with references (the borrow checker rejects it), and Azam (Advanced Rust, chapter 9) shows the workarounds people try and why they fail.
An async block creates exactly such a value. Consider:
async fn first_word_len(text: String) -> usize {
let word = text.split(' ').next().unwrap_or(""); // `word` borrows from `text`...
tokio::task::yield_now().await; // ...and both live across this await point
word.len()
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
println!("{}", first_word_len("Dune Messiah".into()).await);
}
Source: Async Book: Pinning
The compiler turns this function into a state machine that stores both text and word — and word points into
text, inside the same state machine. If the future were moved after it had been polled once, word would dangle. So
the rule is: once a future has been polled, it must stay at the same address until it is dropped.
Pin<P> and Unpin
Pin<P> wraps a pointer P (&mut T, Box<T>, …) and promises that the pointee will not be moved again until it is
dropped. Through a Pin<&mut T> you can call methods that take self: Pin<&mut Self> — such as Future::poll — but you
cannot get a plain &mut T (which would let you mem::swap the value out) unless T: Unpin.
Unpin is an auto trait meaning "this type does not care about being moved, even when pinned". Almost every type is
Unpin: integers, String, Vec, Box<T>, references, and futures written by hand without self-references. The
exceptions are the futures generated by async fn/async blocks and types that opt out with PhantomPinned. For an
Unpin type, Pin has no effect: Pin::new(&mut x) is safe and Pin::get_mut gives the &mut back.
| Situation | What to do |
|---|---|
|
|
|
|
|
|
A trait object future |
|
Source: std::pin
Stack pinning with pin! and heap pinning with Box::pin
.await pins for you, so ordinary async code never touches Pin. You need it when you poll a future by reference — most commonly in a tokio::select! loop that keeps polling the same future across iterations, or when storing futures in
a collection:
use std::future::Future;
use std::pin::{Pin, pin};
use std::time::Duration;
use tokio::time::sleep;
async fn import_catalogue() -> usize {
sleep(Duration::from_millis(120)).await;
1_000
}
#[tokio::main]
async fn main() {
// Stack pinning: poll the same future repeatedly by reference inside select!.
let mut import = pin!(import_catalogue());
let mut ticks = 0;
let rows = loop {
tokio::select! {
rows = &mut import => break rows, // needs Pin<&mut F>: hence pin!
_ = sleep(Duration::from_millis(50)) => {
ticks += 1;
println!("still importing ({ticks})");
}
}
};
println!("imported {rows} rows");
// Heap pinning: store different futures in one collection.
let jobs: Vec<Pin<Box<dyn Future<Output = usize> + Send>>> = vec![
Box::pin(import_catalogue()),
Box::pin(async { 42 }),
];
for job in jobs {
println!("job returned {}", job.await);
}
}
Source: std::pin::pin!
Without pin!, &mut import_catalogue() in select! fails with E0277: "`impl Future<Output = usize>` cannot be
unpinned … consider using the pin! macro". pin! replaced tokio::pin! and futures::pin_mut! as the standard
spelling (stable since 1.68).
Implementing Future for a wrapper: pin projection
When you write your own future that wraps another future — to add a timeout, measure latency, or log — your poll
receives Pin<&mut Self> and must poll the inner future through a Pin<&mut Inner>. Getting from one to the other is
called pin projection. Doing it by hand needs unsafe; the pin-project-lite crate generates the safe projection:
use pin_project_lite::pin_project;
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::time::{Duration, Instant};
pin_project! {
/// Wraps a future and reports how long it took.
struct Timed<F> {
#[pin]
inner: F, // structurally pinned: projected as Pin<&mut F>
started: Option<Instant>, // not pinned: projected as &mut Option<Instant>
label: &'static str,
}
}
impl<F: Future> Future for Timed<F> {
type Output = F::Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<F::Output> {
let this = self.project();
let started = *this.started.get_or_insert_with(Instant::now);
match this.inner.poll(cx) {
Poll::Ready(v) => {
println!("{} took ~{} ms", this.label, started.elapsed().as_millis() / 10 * 10);
Poll::Ready(v)
}
Poll::Pending => Poll::Pending,
}
}
}
fn timed<F: Future>(label: &'static str, inner: F) -> Timed<F> {
Timed { inner, started: None, label }
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
let n = timed("lookup", async {
tokio::time::sleep(Duration::from_millis(30)).await;
7
})
.await;
println!("result {n}");
}
Source: pin-project-lite documentation
The rule behind #[pin]: a field is structurally pinned if the wrapper, once pinned, never moves it — required for
inner futures. Fields that may be moved or replaced freely are left unpinned. tower and tokio use exactly this pattern for
their middleware futures (see Axum: The Tower Service Model).
Opting out of Unpin
A type that is genuinely self-referential (for example, an intrusive linked-list node) declares a PhantomPinned field,
which makes it !Unpin; unsafe code can then rely on the pinning guarantee. You rarely write such types outside
low-level libraries.
use std::marker::PhantomPinned;
struct NotMovable {
_pin: PhantomPinned,
}
fn assert_unpin<T: Unpin>() {}
fn main() {
assert_unpin::<String>();
assert_unpin::<Box<NotMovable>>(); // the Box can move; the value inside stays put
// assert_unpin::<NotMovable>(); // would not compile: `PhantomPinned` cannot be unpinned (E0277)
let _x = NotMovable { _pin: PhantomPinned };
}
Source: std::marker::PhantomPinned
Common errors
| Error | Fix |
|---|---|
E0277 "`...` cannot be unpinned" when polling |
pin
it: |
E0308 expected |
wrap with |
"cannot move out of dereference of `Pin<&mut ..>`" |
project with |
E0599 no method |
call it through a pinned pointer: |