A strong answer
When the Rust compiler lowers async fn example() { let mut buf = [0u8; 64]; let slice = &mut buf[..]; read_into(slice).await; }, it generates a state-machine enum where the AwaitingRead state holds both buf: [u8; 64] AND slice: *mut [u8] pointing at self.buf inside the exact same struct (a self-referential struct)! Before poll() runs the first time, slice has not been created yet, so you can move the future freely in memory. The instant poll() runs and pauses at .await, self.slice stores the raw memory address of self.buf (e.g., 0x7ffd1000). If the caller subsequently moved the future to address 0x7ffd2000 (e.g., via std::mem::swap(&mut fut1, &mut fut2)), fut1.slice would still point to the old address 0x7ffd1000 (dangling pointer / memory corruption!). Pin<P> solves this at compile time: once a !Unpin value is wrapped in Pin<&mut T>, safe code can never obtain a plain &mut T to pass to mem::swap or mem::replace—guaranteeing the object's memory address stays fixed until Drop::drop runs!
Once a !Unpin value (such as a compiler-generated self-referential async block) is pinned at memory address A via Pin<&mut T> (or Box::pin), its bytes remain at address A without ever being moved until its destructor Drop::drop completes. Because moving a value in safe Rust requires either ownership T or a mutable reference &mut T (for mem::swap / mem::replace), and Pin<&mut T> withholds DerefMut whenever T: !Unpin, safe Rust code cannot relocate a pinned !Unpin future after polling begins.
Because Rust futures compile into sized state-machine structs/enums, composing a().await inside b().await nests FutureA inline inside FutureB—resulting in 1 single allocation per top-level spawned task (instead of 1 heap allocation per nested async function call as in JS/Python)! In production systems (Tokio work-stealing async executor, futures::pin_mut! / std::pin::pin!, hyper / axum zero-allocation HTTP state machines, and tokio::select!.), engineers monitor these exact trade-offs to keep p99 latency predictable and prevent resource exhaustion under peak load.
What the interviewer is listening for
- Traces how Rust
Future::pollstate machines,Wakerreadiness, andPin<&mut T>operates in memory step by step rather than giving a vague textbook definition. - States the core correctness invariant and explains why edge cases do not violate it.
- Quantifies time, space, and hardware costs (Nested
async fnCall (a().await), Stack Pin (std::pin::pin!(fut))) with concrete numbers. - Identifies real-world failure modes (Putting a multi-step I/O future
read_message(&mut stream)directly as an arm inside aloop { tokio::select! { ... } }) and explains how production systems mitigate them.
Common mistakes
- Treating Rust
Future::pollstate machines,Wakerreadiness, andPin<&mut T>as a black box without explaining the underlying memory or state transitions. - Ignoring worst-case tail latency or memory overhead (Declaring a 64 KB stack array
let buf = [0u8; 65536];before an.awaitinside anasync fn). - Forgetting boundary conditions and error recovery (Calling blocking
std::thread::sleepor synchronous file/CPU work inside atokio::spawnasync task).