What Is Pin and Why Does Async Rust Need It?

Pin is a smart pointer that prevents memory movement, ensuring async Rust futures remain safe when paused and resumed.

The problem with moving self-references

You are writing an async function that fetches data and caches it. Inside the function, you create a buffer and a pointer to the start of that buffer. The compiler turns this function into a state machine. That state machine holds the buffer and the pointer as fields. When the runtime pauses the function, it saves the state machine. When it resumes, it loads the state machine. If the runtime moves the state machine to a different memory address between pause and resume, the pointer still points to the old address. The pointer dangles. Accessing it causes undefined behavior.

Rust refuses to allow this. The language guarantees memory safety, and dangling pointers violate that guarantee. The tool that enforces this guarantee for async code is Pin. Pin is a wrapper type that prevents a value from being moved in memory. It locks the value in place so that any internal pointers remain valid.

Pin is a type wrapper, not runtime magic

Pin does not perform magic at runtime. It does not glue memory to the hardware. It is a type-level constraint. Pin<P> wraps a pointer P. The wrapper restricts the API you can use to access the value behind the pointer. If you hold a Pin<Box<T>>, you cannot extract a &mut T that would allow you to move the value, unless T implements the Unpin trait.

Most types in Rust implement Unpin. For those types, Pin is effectively a no-op. You can move them freely. The compiler automatically implements Unpin for any type that does not contain self-references. When a type is self-referential, it cannot implement Unpin. The compiler blocks the auto-impl. Pin becomes active. The wrapper prevents moves.

The Unpin trait is a marker. It signals that a type is safe to move even if it is pinned. If you see Pin<T> in a signature, check whether T: Unpin. If it is, the pin is cosmetic. If it is not, the pin is enforcing a safety invariant.

Pin is a contract, not a spell. It relies on the type system to block moves.

How async futures become self-referential

Async functions in Rust compile to state machines. The compiler analyzes the async block and generates a struct with fields for every local variable and a state enum to track progress. When you await a future, the state machine might hold a reference to one of its own fields.

Consider an async function that captures a reference to a local variable. The state machine stores the variable and the reference. The reference points to the variable inside the same struct. This is a self-referential struct. If the state machine moves, the variable moves, and the reference becomes invalid.

The Future trait is defined to be not Unpin by default. This forces callers to pin futures before polling them. The poll method signature requires Pin<&mut Self>. This ensures the future is pinned when it runs. The runtime cannot move the future while it is being polled.

Some futures are safe to move. If a future does not hold self-references, you can add + Unpin to the trait bound. This tells the compiler the future is movable. Many library authors add Unpin to their futures when they are sure no self-references exist. This improves ergonomics for users who want to move futures around.

Self-references are the enemy of movement. Pin locks the door.

Minimal example: Pinning a self-referential struct

Creating a self-referential struct requires unsafe because Rust cannot verify the pointer validity automatically. The example below shows a struct with a pointer to its own data. We use Box::pin to pin the struct safely.

use std::pin::Pin;

/// A struct that holds a pointer to its own data.
/// This is self-referential and fragile if moved.
struct SelfRef {
    data: String,
    ptr: *const str,
}

fn main() {
    // Create the data and pointer separately first.
    // We cannot construct SelfRef directly because the pointer
    // would be invalid until the struct is allocated.
    let mut data = String::from("Hello, Pin!");
    let ptr = data.as_ptr();

    // Construct the struct.
    // The pointer is valid as long as `data` does not move.
    let self_ref = SelfRef { data, ptr };

    // Pin the struct on the heap.
    // Box::pin allocates memory and pins the value there.
    // This is the safe way to create a pinned value.
    let pinned: Pin<Box<SelfRef>> = Box::pin(self_ref);

    // Access the data through the pin.
    // We use `as_ref` to get a Pin<&SelfRef>.
    // This allows us to read the data without moving it.
    let pinned_ref: Pin<&SelfRef> = pinned.as_ref();
    println!("Data: {}", unsafe { &*pinned_ref.ptr });

    // Trying to move the value out of the pin would fail.
    // The compiler rejects code that attempts to extract
    // a mutable reference that could move the value.
    // let moved_data = pinned.into_inner(); // Error: cannot move out of Pin
}

The Box::pin call allocates the struct on the heap and returns a Pin<Box<SelfRef>>. The value is now fixed at that heap address. You cannot move it without going through unsafe. The community convention is to use Box::pin for heap-allocated pinned values. It is safe and concise.

Convention aside: Box::pin(value) is preferred over Pin::new(Box::new(value)). Both work, but Box::pin is the idiomatic form. It signals intent clearly and avoids the intermediate Box that could theoretically be moved before pinning.

Realistic example: Storing a future in a struct

Executors and task runners often store futures inside structs. The future must be pinned because the executor might poll it, drop it, and poll it again. The future could be self-referential. Storing a Pin<Box<dyn Future>> is the standard pattern.

use std::pin::Pin;
use std::future::Future;

/// An executor that stores a task to run later.
struct TaskRunner {
    /// The future is pinned because we might pause and resume it.
    /// Moving it would break internal pointers in the state machine.
    task: Pin<Box<dyn Future<Output = ()> + Send>>,
}

impl TaskRunner {
    /// Create a new runner with a pinned future.
    fn new<F>(future: F) -> Self
    where
        F: Future<Output = ()> + Send + 'static,
    {
        // Box::pin allocates and pins the future.
        // This satisfies the Pin<Box> requirement safely.
        TaskRunner {
            task: Box::pin(future),
        }
    }

    /// Poll the task once.
    /// This simulates an executor step.
    fn step(&mut self, cx: &mut std::task::Context<'_>) -> std::task::Poll<()> {
        // Get a pinned mutable reference to the future.
        // `as_mut` converts Pin<Box<T>> to Pin<&mut T>.
        // This is safe and does not move the value.
        let task_pin: Pin<&mut dyn Future<Output = ()>> = self.task.as_mut();
        task_pin.poll(cx)
    }
}

The TaskRunner stores a Pin<Box<dyn Future>>. The new method uses Box::pin to create the pinned future. The step method uses as_mut to get a Pin<&mut dyn Future> for polling. This pattern appears in every async runtime. The pin ensures the future stays in place while the executor drives it.

Convention aside: The poll method always takes Pin<&mut Self>. This is a hard rule in the Future trait. If you implement Future, your poll signature must use Pin. This forces the caller to pin the future. You cannot poll a future without pinning it first.

Borrowing pinned values safely

Pinned values can be borrowed, but the borrow must respect the pin. You cannot get a &mut T from a Pin<P> unless T: Unpin. Instead, you get a Pin<&mut T>. This preserves the pinning guarantee across the borrow.

The Pin type provides methods to project the pin. as_ref converts Pin<P> to Pin<&T>. as_mut converts Pin<P> to Pin<&mut T>. These methods are safe. They do not allow moving the value. They just change the pointer type while keeping the pin.

If you need to access fields of a pinned struct, you must project the pin to those fields. This is called "pin projection". If a field is not self-referential, you can project the pin to it and get a Pin<&mut Field>. If the field is Unpin, you can even get a &mut Field.

Writing pin projections by hand is error-prone. The community relies on the pin_project crate to generate safe projections. The crate provides a macro that analyzes your struct and generates the projection methods. It handles the unsafe boilerplate and ensures the pin is propagated correctly.

// Example of pin_project usage (conceptual, requires crate).
// use pin_project::pin_project;

// #[pin_project]
// struct MyFuture {
//     #[pin]
//     inner: InnerFuture,
//     data: String,
// }

// impl Future for MyFuture {
//     type Output = ();
//     fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
//         // project! macro generates safe projections.
//         let this = self.project();
//         // this.inner is Pin<&mut InnerFuture>.
//         // this.data is &mut String (because String is Unpin).
//         this.inner.poll(cx)
//     }
// }

The #[pin] attribute marks fields that need to be pinned. The macro generates code that projects the pin to those fields. Fields without #[pin] are treated as Unpin and can be moved. This allows you to build complex self-referential types safely.

Pin projection is the key to building async types. Use the tools the community provides.

Pitfalls and compiler errors

Pinning introduces new failure modes. The compiler catches most mistakes, but some require careful reasoning.

If you try to move a value out of a Pin, the compiler rejects you with an error about moving out of a pinned value. The error message mentions Unpin. If the type does not implement Unpin, you cannot move it. You must use Pin::into_inner if you own the Pin<Box<T>> and want to consume it. into_inner takes ownership and returns the Box<T>. This is safe because you are dropping the pin along with the value.

Using Pin::new on a stack value is dangerous. Pin::new takes a &mut T and returns a Pin<&mut T>. It assumes the value will not move for the lifetime of the reference. If you move the value while the pin exists, you get undefined behavior. The compiler cannot check this. You must ensure the value stays in place. This is why Pin::new is rarely used. Box::pin is safer because the heap allocation does not move.

Using Pin::new_unchecked requires a safety proof. This function creates a Pin without checking anything. It is unsafe. You must guarantee the memory will not be moved. If you violate this, you break memory safety. The // SAFETY comment must list the invariants.

use std::pin::Pin;

/// Wrap a raw pointer in a Pin.
/// This is unsafe because we must prove the memory is pinned.
fn wrap_raw_ptr<T>(ptr: *mut T) -> Pin<*mut T> {
    // SAFETY:
    // 1. The caller guarantees `ptr` points to valid memory.
    // 2. The caller guarantees the memory at `ptr` will not be moved.
    // 3. The caller guarantees the memory is aligned for `T`.
    unsafe { Pin::new_unchecked(ptr) }
}

The // SAFETY comment is a proof. If you cannot write the invariants, you do not have a proof. Do not use new_unchecked.

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

Decision: Pinning strategies

Choose the right pinning tool for your situation. The wrong choice leads to unnecessary complexity or unsafe code.

Use Box::pin when you need to heap-allocate a future or self-referential struct and pin it in one safe step. This is the standard way to store futures in structs or pass them to executors. It handles allocation and pinning atomically.

Use Pin::new when you have a value on the stack that you know will never move for the duration of the pin. This is rare and usually limited to local variables in a single function scope. Only use this if you can prove the value stays in place.

Use Pin::new_unchecked only when you are writing a library that wraps raw pointers or external memory and can prove the memory will not be moved. This requires a // SAFETY comment with a rigorous proof. Reach for this only when safe alternatives do not exist.

Reach for the Unpin trait when you are defining a type that does not contain self-references and want to allow it to be moved freely, even if it implements Future. Adding Unpin improves ergonomics for users who need to move the type.

Use pin_project when you are implementing a custom future or self-referential struct and need to project the pin to fields. The crate generates safe projection code and prevents common mistakes. Do not write pin projections by hand unless you are building the crate itself.

Pin protects the pointer, not the value. Choose the tool that matches your memory layout.

Where to go next