How to Use Pin and Unpin Correctly in Async Rust

Use `std::pin::pin!` to pin a value when you need to borrow it mutably across `.await` points in an async function. Pinning prevents the value from moving in memory, which is required for self-referential types or when using `Pin<&mut T>` in async runtimes.

The moving target problem

You're building an async state machine. You maintain a buffer and a pointer into that buffer. You call .await to fetch the next chunk of data. The runtime suspends the future, moves the entire state machine to a new memory location, and resumes it later. The buffer moved. The pointer now points to garbage. Your program reads random memory and crashes.

This is the core problem Pin solves. Pin prevents values from moving in memory, ensuring that internal pointers remain valid across suspension points. Without pinning, self-referential types are impossible to use safely in async code.

Pinning is a contract, not a lock

Pin is not a mutex. It does not control access. It controls movement. When you wrap a pointer in Pin, you are making a guarantee: the value behind this pointer will not be moved. The compiler enforces this guarantee by restricting how you can interact with the value. You cannot extract a mutable reference. You cannot move the value out of the Pin. You can only access the value through Pin-aware methods.

Think of a value on the stack like a tent. You can pick up the tent and move it anywhere. Pin is like driving a stake through the tent into the ground. Once staked, the tent stays put. You can't move the tent without pulling the stake. The Pin wrapper is the stake.

The type Pin<P> wraps a pointer P. The pointer can be a reference &mut T or a box Box<T>. The Pin wrapper doesn't change the pointer itself. It changes what you can do with the pointee. If the pointee is pinned, the compiler forbids operations that would move it.

Unpin: the opt-out

Most types do not care if they move. A Vec, a String, an i32. These types implement the Unpin trait. Unpin is a marker trait that tells the compiler, "This type is safe to move." When a type implements Unpin, Pin becomes a no-op. Pin<&mut T> is effectively the same as &mut T. You can unwrap it, move the value, and the compiler allows it.

Unpin is an auto-trait. If your type contains only Unpin fields, it automatically implements Unpin. You rarely need to write impl Unpin manually. The compiler does the work for you.

Types that hold pointers to their own interior cannot implement Unpin. If a struct contains a *const u8 pointing into its own Vec<u8>, moving the struct invalidates the pointer. Such types must opt out of Unpin. The compiler prevents Unpin implementation for types with raw pointers or Pin fields by default. This is a safety feature. If a type could move and break its own pointers, it must be pinned.

Convention aside: The community treats Unpin as the default. If you are writing a type and you don't have self-references, you don't touch Unpin. If you have self-references, you ensure Unpin is not implemented. You never implement Unpin for a self-referential type. Doing so breaks the safety contract.

Minimal example: pinning on the stack

Pinning a value on the stack uses the pin! macro. This is the idiomatic way to pin local variables. The macro allocates the value and wraps it in Pin. You cannot move the value out of the macro.

use std::pin::pin;

fn main() {
    // The pin! macro pins the future on the stack.
    // The future cannot be moved after this point.
    let mut future = pin!(async { 42 });

    // You cannot extract the future.
    // This line would fail to compile.
    // let _extracted = future.into_inner();

    // You can poll the future through the Pin wrapper.
    // The runtime uses Pin<&mut T> to poll futures.
    // This guarantees the future won't move during polling.
}

For heap-allocated values, you use Box::pin. This allocates the value on the heap and pins it there. The Box can move, but the value inside the Box cannot. This is essential for async runtimes. The runtime moves the Box around, but the future inside stays fixed in memory.

fn main() {
    // Box::pin allocates and pins in one step.
    // This is the standard way to create pinned futures.
    let pinned_future = Box::pin(async { 42 });

    // The Box can be moved, but the future inside is pinned.
    let _moved_box = pinned_future;
}

Convention aside: Always use Box::pin for futures. Never use Box::new for a future you intend to poll. Box::new gives you a Box<T>, which is not pinned. You would need to wrap it in Pin manually, which is error-prone. Box::pin is the safe, standard choice.

How Pin enforces immobility

The compiler enforces pinning through the API. Pin provides methods that respect the pinning contract. Pin::as_mut gives you a Pin<&mut T> from a &mut Pin<T>. This is a projection. It lets you pass a pinned reference to functions that expect Pin<&mut T>.

If the type implements Unpin, Pin provides get_mut. This returns a &mut T. You can modify the value freely. The compiler knows the value can move, so it allows the mutable reference.

If the type does not implement Unpin, get_mut is unavailable. You cannot get a &mut T. The only way to access the value is get_unchecked_mut, which is unsafe. This method returns a &mut T without checking anything. The caller must guarantee that the value is pinned and will not move. This is where the safety boundary sits. You use unsafe to access the interior, but you must prove the pinning holds.

use std::pin::Pin;

fn access_pinned<T>(pinned: Pin<&mut T>) {
    // If T is Unpin, get_mut is available.
    // If T is not Unpin, this line won't compile.
    // let _ref = pinned.get_mut();

    // get_unchecked_mut is always available, but unsafe.
    // You must ensure the value is pinned.
    // let _ref = unsafe { pinned.get_unchecked_mut() };
}

Pinning is a promise, not a shield. Pin does not make raw pointers safe. You still need unsafe to dereference them. Pin only ensures the address is stable. If you dereference a raw pointer inside a pinned value, you must verify the pointer is valid. The pinning guarantee helps, but it doesn't replace pointer safety checks.

Realistic example: a self-referential future

Self-referential types are rare in safe Rust, but they appear in async code. Futures are state machines. The compiler generates a struct with fields for the state and local variables. If the future holds a pointer to its own buffer, it is self-referential. The Future trait requires Pin<&mut Self> in the poll method. This ensures the future is pinned when polled.

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

struct SelfRefFuture {
    buffer: Vec<u8>,
    // This pointer points into buffer.
    // Moving SelfRefFuture would invalidate cursor.
    cursor: *const u8,
}

// SAFETY: SelfRefFuture is self-referential.
// It must not move while cursor is valid.
// We do not implement Unpin to enforce this.

impl Future for SelfRefFuture {
    type Output = ();

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
        // SAFETY: We access cursor only through Pin.
        // Pin guarantees buffer won't move, so cursor stays valid.
        // We must not move SelfRefFuture after this access.
        let this = unsafe { self.get_unchecked_mut() };

        // Access buffer via cursor.
        // This is safe because Pin ensures immobility.
        let _val = unsafe { *this.cursor };

        Poll::Ready(())
    }
}

The poll method takes Pin<&mut Self>. This is the contract. The runtime pins the future before polling. You can rely on the future not moving during the poll. If you need to access self-referential fields, you use get_unchecked_mut inside unsafe. The SAFETY comment documents the invariant: the value is pinned, so internal pointers remain valid.

Treat the SAFETY comment as a proof. If you can't write the invariant, you don't have one.

Pin projections

Real async code often needs to project Pin through fields. If you have Pin<&mut Outer>, you might need Pin<&mut Inner> for a field. This is called pin projection. You cannot simply take a reference to the field. The field might not be pinned. You must project the Pin wrapper.

Manual pin projections are tedious. You write a project method that returns pinned references to all fields. The community uses the pin-project crate to automate this. The crate generates the projection code for you. It handles the Unpin bounds correctly.

Convention aside: Use pin-project for any struct that implements Future or contains Pin fields. Writing manual projections is error-prone. The crate is the standard tool. It adds a small dependency, but it saves bugs.

Pitfalls and compiler errors

You try to extract a value from Pin. You call into_inner(). The compiler rejects you with E0509 (cannot move out of type which does not implement Unpin). This error means the type is pinned and cannot be moved. You must drop the Pin or use Unpin to extract the value.

You try to get a mutable reference. You write &mut *pinned. The compiler rejects this. You cannot dereference Pin to get a mutable reference. You must use as_mut() or get_unchecked_mut(). The compiler enforces the pinning contract at the type level.

You assume Pin makes everything safe. Pin does not validate pointers. It only prevents movement. If you have a dangling pointer inside a pinned value, dereferencing it is still undefined behavior. Pin helps by ensuring the pointer doesn't become dangling due to movement, but it doesn't fix existing dangles.

You forget that Pin is not Copy. Pin<&mut T> is not Copy. You cannot clone a pinned mutable reference. You must use as_mut() to create a new Pin<&mut T> from a &mut Pin<T>. This is a projection, not a clone.

When to use Pin

Use Pin<&mut T> when you are implementing a self-referential type and need to guarantee the memory address stays constant across borrows. Use Box::pin when you need to allocate a future on the heap and pass it to an executor; this pins the future so the runtime can move the Box without moving the future inside. Use the pin! macro when you have a local variable that must be pinned on the stack, such as a future you are polling manually. Use Unpin marker trait when you are defining a type that contains no self-references and want to allow the compiler to optimize away Pin overhead. Reach for RefCell or interior mutability when you need to mutate data behind an immutable reference; Pin does not provide mutability, it only provides immobility.

Trust the borrow checker. It usually has a point. If the compiler complains about Pin, check your self-references. If you don't have self-references, you probably don't need Pin.

Where to go next