Rust Ownership & Mutability

Sugih Jamin in conversation with Gemini
February 28th, 2026 and March 4th, 2026

As with C, Rust differentiates mutability of pointer vs mutability of pointed memory: | C Syntax | Rust Syntax | Meaning | | :— | :— | :— | | const int *p | let mut p: &i32 | Mutable pointer to immutable data. | | int *const p | let p: &mut i32 | Immutable pointer to mutable data. | | const int *const p | let p: &i32 | Immutable pointer to immutable data. | | int *p | let mut p: &mut i32 | Mutable pointer to mutable data. |

A “container” (array, dictionary, string, struct, and closure) owns the data held in its memory and Rust follows the Inherited Mutability principle: if you own a box, and that box is marked mutable, you have permission to reach inside and mutate its contents. Rust intentionally couples these to avoid the confusion where a pointer might be const but the data it points to is accidentally modified through an alias.

If you want a variable that is “immutable” on the outside but has “mutable” memory inside, you use types like Cell or RefCell. This allows you to:

In Rust, we call this Interior Mutability. Using RefCell, you can mutate data even if the variable holding it is declared with let (immutable). This is used for specific patterns like the Observer Pattern or when you need to satisfy a trait that expects an immutable reference but you truly need to update an internal counter or cache. This moves the borrow checking from compile-time to runtime.

Closure

In Rust, a closure is not just a function pointer; it is effectively an anonymous struct that “contains” its environment.

Closure-as-a-Container:

  1. The “Fields”: Every variable you mention inside the closure becomes a field in this hidden struct.
  2. The “Storage”:
    • If it captures by reference (&T or &mut T), the struct contains a pointer.
    • If it captures by value (using the move keyword), the struct actually owns the data—-the memory is moved into the struct.
  3. The Mutability: This is where the “container” part matters most. Because the closure is a struct, the Inherited Mutability rules apply to it:
    • If the “container” (the closure variable) is immutable (let), you cannot modify its internal fields.
    • If the “container” is mutable (let mut), you can modify its internal fields.

The “Hidden Struct” Visualization

For:

let mut s = String::from("hello");
let mut closure = || { 
    s.push_str(" world"); // Modifies s 
};

closure desugars to:

// 1. The compiler generates this struct "container"
struct MyClosure<'a> {
    s: &'a mut String, // A field containing the reference
}

// 2. The closure body becomes a method on this struct
impl<'a> FnMut<()> for MyClosure<'a> {
    fn call_mut(&mut self) {
        self.s.push_str(" world"); // Reaching into the container to mutate
    }
}

// 3. Your 'let mut closure' is just an instance of this struct
let mut closure = MyClosure { s: &mut s };

The compiler infers the mutability of s based on how you use the variable within the closure’s body.

  1. Usage-Based Inference

    The compiler looks at what the closure does with s. Because you call s.push_str(), which requires a mutable reference (&mut self), the compiler determines that the closure must capture s by mutable reference.

  2. The Internal “Struct”

    When you define the closure, Rust effectively generates a “hidden” struct to hold its environment. For the above code, it looks something like this:

    struct Closure<'a> {
       s: &'a mut String, // Captured as a mutable reference
    }
    

    This is why the original s must be declared as let mut s—-the closure needs to be able to take that mutable borrow from the start.

  3. Why the closure itself must be mut

    Because the closure’s internal state (the reference to s) must be modified or used to perform a mutation, the closure implements the FnMut trait.

    • The call_mut method of this trait requires a mutable reference to the closure itself (&mut self).
    • Therefore, you must declare the closure as let mut closure to ever actually call it.

Summary of the Chain:

  1. Body uses push_str -> Requires &mut String.
  2. Compiler captures s via &mut -> Closure now “contains” a mutable reference.
  3. Closure implements FnMut -> Calling it modifies its internal state (the borrow).
  4. Requirement -> The closure variable itself must be mut to be invoked.

Adding move

let mut s = String::from("hello");
let mut closure = move || { 
    s.push_str(" world"); // Modifies s 
};

closure desugars to:

// Before (Borrow)
struct ClosureBorrow<'a> {
    s: &'a mut String, // Just a pointer
}

// After (Move)
struct ClosureMove {
    s: String, // The closure now OWNS the string data
}

Even though the closure now owns the string, you still need let mut closure because calling the closure still involves reaching into that “container” struct and calling push_str on the field it owns.

Selective move

In Rust, the move keyword is an “all or nothing” switch—-it forces the closure to take ownership of every variable it uses from the environment. To achieve a “selective” move where you move one variable but only borrow another, you use variable rebinding just before defining the closure.

If you want to move data_to_move but only borrow data_to_share, you explicitly create a reference for the shared data and then use a move closure.

let data_to_move = String::from("I am being moved");
let mut data_to_share = String::from("I am being borrowed");

// 1. Create an explicit reference for what you want to borrow
let shared_ref = &mut data_to_share;

// 2. Use a 'move' closure
// 'data_to_move' is moved in (ownership transfer)
// 'shared_ref' is also moved in (but it's just a pointer!)
let mut closure = move || {
    println!("Moved: {}", data_to_move);
    shared_ref.push_str("... and modified!");
};

closure();

// println!("{}", data_to_move); // ERROR: This would fail, it's gone.

// // WORKS: Still exists here, but ONLY IF 'closure' is never used again (NLL).
// If you called 'closure()' again below, this line would trigger a compile error.
println!("Shared: {}", data_to_share); 

The Alternative:

If you don’t want to use the move keyword because you have many variables to borrow and only one to move, you can perform a “dummy move” inside a standard closure:

let to_move = String::from("Move me");
let to_borrow = String::from("Borrow me");

let closure = || {
    let _internal_move = to_move; // This forces 'to_move' to be captured by value
    println!("Borrowed: {}", to_borrow); 
};

References

Master Reference

| Rust Pattern | External Owner | Thread-Safe? | Lifetime | Data Mutability | Pointer Mutability | C Equivalent | | :— | :— | :—: | :— | :— | :— | :— | | let mut x: T | Unique (Stack) | Send, not Sync | Stack/Scope | Mutable | Mutable | T x; | | let x = &mut *storage | Borrowed | No | Tied to Owner | Mutable | Immutable | T * const x | | let mut x = &mut *storage | Borrowed | No | Tied to Owner | Mutable | Mutable (only if Lifetime ≥ ‘Original) | T * x | | Cell<T> | Unique (Stack) | Send, not Sync | N/A | Mutable (Copy) | Immutable | struct { T val; } | | RefCell<T> | Unique (Stack) | Send, not Sync | N/A | Mutable (Runtime) | Immutable | struct { T val; lock; } | | Box<T> | Unique (Heap) | Yes (if T: Sync) | Heap (Owned) | Immutable | Immutable | const T * const x | | let mut x = Box<T> | Unique (Heap) | Yes (if T: Sync) | Heap (Owned) | Mutable | Mutable | T * x | | Box<Cell<T>> | Unique (Heap) | Send, not Sync | Heap (Owned) | Mutable (Copy) | Immutable | T * const x (memcpy) | | Box<RefCell<T>> | Unique (Heap) | Send, not Sync | Heap (Owned) | Mutable (Runtime) | Immutable | T * const x (w/ check) | | Rc<T> | Shared (Heap) | No | Heap (Ref-Count) | Immutable | Immutable | const T * const x | | Rc<RefCell<T>> | Shared (Heap) | No | Heap (Ref-Count) | Mutable (Runtime) | Immutable | T * const x (w/ RC) | | Arc<T> | Shared (Heap) | Yes (if T: Sync) | Heap (Atomic) | Immutable | Immutable | const T * const x | | Arc<Mutex<T>> | Shared (Heap) | Yes | Heap (Atomic) | Mutable (Locked) | Immutable | T * const x (w/ Mutex) | | let x: Pin<Box<T>> | Unique (Heap) | Yes (if T: Sync) | Heap (Owned) | Mutable (if Unpin) | Immutable | N/A | | let mut x: Pin<Box<T>> | Unique (Heap) | Yes (if T: Sync) | Heap (Owned) | Mutable (if Unpin) | Mutable | N/A | For “Thread-Safe?” assumes T is Send in all cases.

Ownership

Who is responsible for dropping the memory, determines Lifetime.

Mutability

Inherited Mutability:

In contrast to languages where pointer-constness and data-constness are explicitly separated (int *const x vs const int *x in C), mutability in Rust is a property of the binding and the path to the data, not the data type itself.

When you own a Box<T>, you own the T. Therefore, the mutability of the T is inherited from the mutability of the Box.

Box is a transparent wrapper. Rust’s goal is to make a Box<i32> behave as much like a plain i32 as possible, just located on the heap. If you had a local variable let mut x = 5;, you can change its value. Rust views the heap data inside a Box as just an extension of that variable. If you “own” the memory, you “own” the right to define its mutability.

How Deref Handles Mutability

“Automatic dereferencing” is split into two distinct traits:

Fixed Address, Mutable Content

#![allow(unused)]
fn modify_value(ptr: &mut i32) {
    *ptr += 100; // Change the data
    // ptr = &mut 500; // ERROR: Cannot reassign the local variable 'ptr' 
                      // because the BINDING 'ptr' is not 'mut'.
                      // AND must always points to storage with ptr's 
                      // original lifetime at owner
}

fn main() {
    let mut x = Box::new(5);
    let mut y = Box::new(-1);
    *x = 10;            // Change data
    
    /* #1
    let x = x;          // RE-BIND as immutable (Shadowing)
    x = y;   // ERROR: The address is locked
    *x = 20;         // ERROR: and the data is locked.
    */
    
    let x = &mut *x;    // 'x' cannot be reassigned to a new address
    /* #2
    x = &mut *y;   // ERROR: The address is locked
    *x = 20;            // but the data is changeable.
    */
    
    // #3
    //modify_value(x); 
    
    println!("{}", x); // Output: 110
}

Interior Mutability:

Capability granted by the container

Interior Mutability Wrappers

Cell and RefCell bypass the let / let mut restriction by managing access themselves.

Rc, Cell, and RefCell are restricted to a single thread to prevent data races.

Thread Safety:

Determined by Arc and Mutex.

Generators with UnpinCell

The Interface Conflict: The Iterator::next method takes &mut self, which implies the iterator can be moved between calls. To support self-references (like a generator holding a reference to its own internal buffer across a yield), the object must be pinned.

The Problem (The Pinning “Blockade”) Currently, if you have a Pin<&mut MyStruct>, you cannot get a &mut Field to any field inside it unless the field is Unpin. This makes it very hard to write generators that need to “swap” data or move internal buffers.

We are constantly fighting between Ownership (moving things) and Pinning (staying put).

Currently, an Iterator must be Unpin. If we had UnpinCell, we could make an Iterator that contains self-referential (Pinned) state but exposes its data through an Unpin interface.

struct MyGenerator {
    // This field is "pinned" (it has internal pointers)
    internal_state: SomeSelfReferentialType, 
    
    // This field is wrapped in UnpinCell
    // It tells Rust: "Even if MyGenerator is Pinned, you can still 
    // get a normal &mut i32 to this field."
    data: UnpinCell<i32>, 
}

impl MyGenerator {
    fn work(self: Pin<&mut Self>) {
        // 1. We can't move 'internal_state' (Safety!)
        
        // 2. But we CAN get a normal mutable reference to 'data'
        // without using unsafe code!
        let val: &mut i32 = self.data.get_mut(); 
        *val += 1;
    }
}

“Static Frame” versus “Dynamic Data” model:

  1. The “Skeleton” vs. The “Payload”

    In a complex, self-referential generator (the state machine), the compiler creates a “Skeleton” of pointers that must never move because they point to other fields within that same struct.

    • The Skeleton (Pinned): These are the internal state variables, the “yield points,” and the pointers that allow the generator to resume exactly where it left off. If these move, the addresses break.
    • The Payload (UnpinCell): These are the actual values you are calculating—the integers, strings, or buffers. These values usually have no reason to be “bolted to the floor.”

    By wrapping the Payload in UnpinCell, you “deconstruct” the struct’s strict pinning requirements. You tell the compiler: “Keep the skeleton pinned, but let me reach in and move the payload around as if it were a normal, movable variable.”

  2. Yielding and Sharing with Futures

    The exact reason why this is necessary: The Hand-off.

    • Yielding: When a generator yields a value, that value often needs to be moved out of the generator and into the caller’s scope. If the value is “trapped” inside a pinned struct, Rust’s current rules make it very difficult to move it out without using Option::take or unsafe. UnpinCell makes this “move-out-of-pin” operation safe.
    • Sharing with Futures: In an async gen block, you might want to pass a value to an async closure or a sub-Future. That sub-Future might require exclusive ownership (&mut T) of the data.

    Without UnpinCell, the fact that the generator is pinned effectively “infects” every field inside it, making it impossible to give a simple &mut T to a sub-Future. UnpinCell acts as a “Pinning Shield” that protects the payload from the skeleton’s constraints.

  3. The “Lending” Bridge

    With UnpinCell, we can finally build Lending Iterators that are actually ergonomic. a. The Iterator (the Skeleton) stays Pinned in memory. b. The Item it produces (the Payload) is stored in an UnpinCell. c. The next() method can return a mutable reference to that item because UnpinCell allows us to “extract” a normal &mut T from a Pin<&mut Self>.

UnpinCell doesn’t move memory

The value inside the cell stays at a fixed memory offset within the struct, but the compiler allows you to treat it as if it could move.

To understand how a mutable reference tracks the item, we have to look at the difference between Physical Movement and Semantic Permissions.

  1. The “Fixed offset” Reality

    Inside a Pin<&mut MyStruct>, the UnpinCell<T> field is physically “bolted to the floor” at a specific byte offset (e.g., 16 bytes from the start of the struct).

    • The Skeleton (Pinned): Cannot move.
    • The Payload (UnpinCell<T>): Also cannot physically move (because it’s part of the pinned struct).

    So why do we say it “can move”? Because UnpinCell gives you the semantic permission to use functions like std::mem::replace or std::mem::swap on that memory.

  2. Immovable Mutable Reference

    When you call self.payload.get_mut(), you get a standard &mut T.

    • This reference points to that fixed physical address (e.g., 0x1000).
    • As long as that &mut T exists, the Borrow Checker ensures nobody else can touch that memory.
    • The item doesn’t “move” while you hold the reference; it stays at 0x1000.
  3. The “Swap” Illusion

    The “movement” happens when you use that &mut T to perform a Swap.

     let r: &mut T = pin_struct.payload.get_mut(); // Points to 0x1000
        
     let new_value = T::new();
     std::mem::swap(r, &mut new_value); 
    

    What happened here?

    1. The physical memory at 0x1000 (the “Payload” slot) now contains the new_value.
    2. The old value was moved into the local new_value variable on the stack.
    3. The reference r was always pointing at the “slot” (0x1000), not the specific instance of the data.
  4. Why this is forbidden without UnpinCell

    In a normal pinned struct (the Skeleton), fields might contain pointers to each other.

    • Field A points to Field B.
    • If you “Swap” Field B for a new value, Field A is now pointing to a valid address, but the data it expected (the internal state of B) has been replaced by something else.
    • This is a Logic/Safety Violation.

    UnpinCell tells the compiler: “There are no internal pointers pointing to this specific field. It is safe to swap the contents of this slot because no other part of the Skeleton is ‘watching’ this specific memory address.”

    Field B is not allowed to be an UnpinCell. The compiler does not check if the content of an UnpinCell is an address. You are allowed to put a reference (&T) or a pointer inside an UnpinCell. However, the compiler does enforce a much stricter rule: Internal Pointers (Self-References) are not allowed to point into an UnpinCell.

The “Deconstruction” Answer

The UnpinCell doesn’t allow the item to “drift” around memory while you are using it. Instead, it allows you to replace the contents of a pinned slot without violating the rules of the larger pinned structure.

This is exactly how Generators yield values:

  1. The Generator (Skeleton) stays pinned at 0x1000.
  2. It calculates a value and puts it in its UnpinCell “Yield Slot” at 0x1010.
  3. The next() method gives the caller a &mut T to that slot.
  4. The caller can then swap the value out of that slot and into their own local variable.

Pinned Places

This makes “Pinned” a first-class citizen of the grammar, like mut.

With Pinned Places:

  1. The Refined Analogy

    • UnpinCell<T>: You have a building where everything is technically protected by a “Historic Landmark” status (The Pin). You have to apply for a special permit (UnpinCell) just to move a single chair or paint one door. It’s a localized exception to a global restriction.
    • Pinned Places: You define the Structural Load-Bearing Columns (the fields with self-references) as pin. The rest of the building (the other fields) remains “Normal.” The “Building Code” (The Compiler) only enforces immovability on the specific columns you tagged.
  2. Why “Pinned Places” is the “Final Boss” of Ergonomics

    The “Lament” of Pin today is that it is infectious. If you pin a struct, the entire struct is pinned. You lose the ability to easily get an &mut T to any of its normal fields.

    With Pinned Places:

    1. Granularity: You only tag the “Skeleton” fields (the internal pointers) as pin.
    2. Inherited Freedom: Any field not tagged pin stays “Unpin” by default.
    3. No Projections: You don’t need the pin-project macro or a “Hand-off” via UnpinCell. You just access the field. If it’s a pin field, you get a Pin<&mut T>. If it’s a normal field, you get a standard &mut T.
     // Hypothetical Rust 2027 Syntax
     struct MyGenerator {
         // THE SKELETON: Load-bearing, self-referential
         pin internal_ptr: *const i32,
            
         // THE PAYLOAD: Furniture, movable, standard &mut access
         data: i32, 
     }
        
     impl MyGenerator {
         // 'self' is in a Pinned Place.
         // The compiler knows 'internal_ptr' is pinned, but 'data' is not.
         fn poll_next(pin self: &mut Self) -> Poll<Option<i32>> {
             // Direct access! No .project() macro needed.
             let val: &mut i32 = &mut self.data; 
                
             // This remains a Pinned reference automatically
             let ptr: Pin<&mut *const i32> = &mut self.internal_ptr;
                
             *val += 1;
             Poll::Ready(Some(*val))
         }
     }
    

Pin projection

In Rust, pin projection transforms a pinned reference to a struct into references to its individual fields, maintaining the pinning guarantee for specific “structural” fields while allowing normal access to others. The following comparisons show how a struct with one pinned field (timer) and one unpinned field (completed) is projected using current library solutions and major language proposals.

  1. pin-project Crate (Current Standard)

The [pin-project]https://docs.rs/pin-project/latest/pin_project/attr.pin_project.htm) crate uses a procedural macro to generate a “shadow” projection struct.

Before Projection:

You have a single opaque wrapper Pin<&mut MyFuture>.

#[pin_project]
struct MyFuture<Fut> {
    #[pin] timer: Fut,   // Structurally pinned
    completed: bool,     // Not pinned
}

After Projection:

Calling .project() returns a new type where the fields have been split.

// Inside a method: let this = self.project();
struct MyFutureProjection<'a, Fut> {
    timer: Pin<&'a mut Fut>, // Pinned projection
    completed: &'a mut bool, // Normal mutable reference
}
  1. Pinned Places (Proposed Syntax)

The pinned places proposal (by withoutboats) suggests making pinning a property of the memory location itself rather than just a pointer wrapper.

Before Projection:

// A "pinned place" on the stack
let pinned mut my_fut: MyFuture<Fut> = ...; 

After Projection:

The compiler would automatically understand which fields are structural. Projection would ideally use native reference syntax.

// Projection happens via field access on the pinned place
let timer_ref: &pinned mut Fut = &pinned mut my_fut.timer;
let comp_ref: &mut bool = &mut my_fut.completed;
  1. MinPin (Proposed Syntax)

The MinPin proposal (by Niko Matsakis) introduces pinned as a type modifier to make Pin behave more like a first-class reference.

Before Projection:

struct MyFuture<Fut> {
    pinned timer: Fut, // Keyword marks structural pinning
    completed: bool,
}

After Projection:

MinPin aims for “automatic reborrowing,” allowing you to use fields directly if you hold a pinned reference to the parent.

impl<Fut> MyFuture<Fut> {
    fn poll(self: pinned &mut Self) {
        // Pinned field projects to a pinned reference automatically
        let _: pinned &mut Fut = &pinned mut self.timer; 
        
        // Unpinned field projects to a normal reference
        let _: &mut bool = &mut self.completed;
    }
}

Summary Comparison Table

Implementation Pinned Field Becomes Unpinned Field Becomes Projection Method
pin-project Pin<&mut T> &mut T Manual .project() call
Pinned Places &pinned mut T &mut T Native &pinned mut op
MinPin pinned &mut T &mut T Implicit via field access

Custom Future Implementation

Implementing a custom Future with the pin-project crate allows you to safely access fields of a pinned struct without using unsafe code. This is essential when your future wraps an inner future (structural pinning) or needs to modify its own internal state (non-structural pinning).

In this example, TimedFuture wraps an inner future and tracks when it started. The inner field must be pinned to be polled, while start_time can be accessed as a normal mutable reference.

use pin_project::pin_project; // 1.1
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::time::Instant;

#[pin_project]
pub struct TimedFuture<F> {
    #[pin]
    inner: F,          // Structural: we need Pin<&mut F> to call .poll()
    start_time: Instant, // Non-structural: we only need &mut Instant
}

impl<F: Future> Future for TimedFuture<F> {
    type Output = (F::Output, std::time::Duration);

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        // 1. Perform the projection
        // 'this' is a generated struct where 'inner' is Pin<&mut F> 
        // and 'start_time' is &mut Instant.
        let this = self.project();

        // 2. Poll the inner future using its pinned reference
        match this.inner.poll(cx) {
            Poll::Ready(output) => {
                let duration = this.start_time.elapsed();
                Poll::Ready((output, duration))
            }
            Poll::Pending => Poll::Pending,
        }
    }
}

To use this future, you wrap another asynchronous operation and .await the result.

async fn example_usage() {
    let inner_future = async {
        // Simulate some async work
        tokio::time::sleep(std::time::Duration::from_millis(100)).await;
        "Done!"
    };

    let timed = TimedFuture {
        inner: inner_future,
        start_time: Instant::now(),
    };

    let (result, duration) = timed.await;
    println!("Result: {} took {:?}", result, duration);
}

Key Components

Manual pin projection

If you have a pinned struct and need to project it to its fields without the crate, you would implement it like this:

use std::pin::Pin;

struct MyFuture<F> {
    inner: F,       // We want this to be structurally pinned
    completed: bool, // This will be unpinned
}

impl<F> MyFuture<F> {
    // Structural projection: Pin<&mut Self> -> Pin<&mut F>
    fn project_inner(self: Pin<&mut Self>) -> Pin<&mut F> {
        // SAFETY: We must guarantee that 'inner' is only ever 
        // accessed via a Pin as long as the parent is pinned.
        unsafe { self.map_unchecked_mut(|s| &mut s.inner) }
    }

    // Non-structural projection: Pin<&mut Self> -> &mut bool
    fn project_completed(self: Pin<&mut Self>) -> &mut bool {
        // SAFETY: 'completed' is not structurally pinned, so we 
        // can safely get a raw &mut to it.
        unsafe { Pin::get_unchecked_mut(self).completed }
    }
}

But still no yield from or yield* for Rust

yield cannot cross closure boundaries:

In Rust, every async gen block is its own statically sized struct.

Why AsyncIterator is not stable yet?

The path to stabilization in the Rust standard library is blocked by three massive “Real World” technical hurdles. Rust’s “Stability Guarantee” means that once these are in std, they can never be changed.

  1. The “Lending” Iterator Conflict (The Biggest Blocker)

    The current AsyncIterator trait in std returns an Owned item: fn poll_next(self: Pin<&mut Self>, ...) -> Poll<Option<Self::Item>>

    However, many high-performance use cases (like zero-copy networking) require a Lending Iterator (also called a “Borrowing Iterator”).

    • The Issue: A Lending Iterator returns an item that borrows from the iterator itself.
    • The Conflict: The current Iterator and AsyncIterator designs don’t support this. If they stabilize AsyncIterator now as a “non-lending” trait, they might split the ecosystem into two incompatible types of streams.
    • The Goal: They are trying to find a way to make AsyncIterator “Generic over Lending” using GATs (Generic Associated Types), but this is proving to be a compiler-logic nightmare.
  2. The “Trait Alias” and Ecosystem Fragmentation

    There is a massive amount of code already using the futures::Stream trait.

    • The Risk: If std::async_iter::AsyncIterator is slightly different from futures::Stream, the entire Rust ecosystem will break in half.
    • The Work: The team is currently working on “Trait Transformer” logic that would allow Stream to automatically “count” as an AsyncIterator. This requires deep changes to how the compiler handles trait matching (Specialization), which is not yet stable.
  3. The Drop Problem (Async Drop)

    This is the “dark side”.

    • The Scenario: What happens if you are halfway through an async gen loop and you suddenly break?
    • The Crisis: The generator’s state machine is currently suspended at a yield point. To clean up, it needs to run its destructors (Drop). But in an async context, those destructors might need to be async (e.g., closing a database connection).
    • The Gap: Rust does not have Async Drop yet. Stabilizing AsyncIterator without a solution for how to “Async Drop” a cancelled generator could lead to resource leaks or “blocking-in-async” bugs that are impossible to fix later. Nuance: Technically, AsyncIterator could be stabilized without Async Drop. However, the gen blocks (the ergonomic driver) cannot be safe without Async Drop. Since the project wants gen blocks to be the primary way users write iterators, they are effectively coupled.
  4. Recent Activity: The “Async 2024” Push

    While individual issues might look old, the project is currently in its most active phase in years. The Rust Project Goals for 2025/2026 highlight that “Async-Sync Parity” is a Flagship Goal.

    While the syntax (gen) and primitives (Async Closures) are arriving, the Trait Definition (AsyncIterator) is being held back to ensure it can support Lending patterns (GATs) and Safe Cancellation (Async Drop).

    Recent major milestones include:

    • Rust 1.85 (Feb 2025): Stabilization of Async Closures, a feature that had been in development for years to solve the “Lending” problem.
    • Rust 2024 Edition (Current March 2026): The introduction of the gen keyword, reserving the syntax needed for native generators.
    • Async Drop Progress (2026): Ongoing work under the “Feature Gate”![feature(async_drop)] to finally solve the cancellation safety issue.

References: