rgoussu@goussu: ~/library/rust/ownership-and-concurrency
~/library/rust/ownership-and-concurrency cat ownership-and-borrowing.md

Ownership & borrowing — the rules and the fights

# The core rules in depth — moves and Copy, Drop and RAII, the borrow rules, NLL and Polonius — plus the recurring borrow-checker fights and their idiomatic resolutions.

Conceptsaved 2026-08-09 #rust#ownership#borrow-checker#raii

Overview

Three rules carry the whole system: every value has exactly one owner; when the owner goes out of scope the value is dropped; and at any moment a value has either any number of shared references (&T) or exactly one exclusive reference (&mut T) — never both. Everything else in this group is machinery for expressing programs inside those rules. The practical skill is not memorizing them (chapter 4 of the Book does that in an afternoon) but recognizing the recurring fights — the patterns where the checker rejects your first design — and knowing the idiomatic resolution for each, because the checker is usually critiquing the design, not the syntax.

Key points

  • Moves are the default: assignment and argument-passing transfer ownership; the source becomes unusable (a compile error, not a runtime nullity). Types opt into copy semantics with Copy (bit-copyable, no Drop — integers, &T, small structs of the same); everything else moves.
  • Drop is deterministic RAII: owners drop at scope exit in reverse declaration order, struct fields in declaration order; Drop::drop is where files close, locks release, buffers free. This is the "no GC" half — memory and every other resource share one cleanup mechanism, with no finalizer-timing question to reason about.
  • Borrows are compile-time reader/writer locks: &T shared/read-only, &mut T exclusive. The exclusivity of &mut is what makes iterator invalidation, torn updates and (later, across threads) data races impossible — and it's why the compiler can optimize aggressively around noalias guarantees.
  • NLL — borrows end at last use: since the 2018-era non-lexical-borrowck rework, a borrow's region ends where it's last used, not at the closing brace — most "but the borrow is obviously over" complaints predate NLL. Polonius, the next-generation formulation, exists to accept a further class of obviously-fine programs (the famous "get-or-insert" pattern); parts are landing incrementally.
  • Reborrows and two-phase borrows grease the common cases: &mut implicitly reborrows for a call (vec.push(vec.len()) compiles thanks to two-phase borrows); understanding "this is a reborrow, not a second borrow" dissolves a family of confusing errors.
  • Disjoint borrows are field-granular: the checker tracks struct fields separately (&mut self.left and &mut self.right coexist), and since edition 2021 closures capture individual fields rather than whole structs — but method calls borrow all of self, which is why extracting fields into locals (or splitting the struct) unsticks so many designs.
  • clone() is a tool, not a defeat: while learning — and often permanently — cloning a String on a cold path is the right trade; the checker's job is making the copy visible, not forbidding it. Reach for references when profiling says so.
  • The checker as design feedback: persistent fights usually mean shared mutable state is load-bearing in the design; the fixes below are all ways of making ownership boundaries explicit — which is why post-fight designs tend to be better even judged without the checker.

A value's states under the three rules:

stateDiagram-v2
    state "Owned" as Owned
    state "Shared borrows active (read-only, any number)" as Shared
    state "Exclusive borrow active (exactly one)" as Exclusive
    state "Moved (source unusable)" as Moved
    state "Dropped" as Dropped
    [*] --> Owned
    Owned --> Owned: Copy types are bit-copied, source stays usable
    Owned --> Moved: assignment or argument passing (non-Copy)
    Owned --> Shared: shared references taken
    Shared --> Owned: last use of the borrows (NLL)
    Owned --> Exclusive: exclusive reference taken
    Exclusive --> Owned: last use of the borrow (NLL)
    Owned --> Dropped: owner goes out of scope
    Dropped --> [*]: Drop runs, memory and other resources freed
    note right of Moved
        Using the moved-from source is a
        compile error, not a runtime nullity.
    end note
    note right of Exclusive
        Any number of shared references or
        exactly one exclusive reference -
        never both at once.
    end note

Details

The recurring fights and their resolutions

The fight The error smell Idiomatic resolution
Returning a reference to a local "returns a value referencing data owned by the current function" Return the owned value; let the caller borrow it
Get-or-insert on a map "cannot borrow as mutable more than once" The entry() API — map.entry(k).or_insert_with(…)
Moving a field out of &mut self "cannot move out of borrowed content" std::mem::take/replace — swap a default in, own the old value
Two &mut into one struct via methods method calls borrow all of self Split the struct, free functions over fields, or borrow fields as locals first
Self-referential struct "borrowed value does not live long enough", forever Don't — store indices/keys; or Pin + a crate (ouroboros) when unavoidable
Graphs & doubly-linked structures ownership wants a tree Arena + indices (Vec<Node>, generational-arena, slotmap), or Rc<RefCell<…>> accepting runtime checks
Long-lived iterator over a collection you mutate classic invalidation, caught Collect keys first, drain, or restructure the loop — the checker just found a real bug
Callback stored while borrowing environment closure lifetime tangles Own the state in the closure (move), share via Rc/Arc

Reading the errors

rustc --explain E0382 (use of moved value), E0499 (two &mut), E0502 (shared + mutable) cover most of daily life — the error-code culture means each has a worked explanation, and the diagnostics almost always name the fix ("consider cloning", "consider using split_at_mut"). Trust them; they're written by the same people who wrote the passes.

Examples

// mem::take — move a field out of &mut self without leaving a hole
fn finish(&mut self) -> Vec<Event> {
    std::mem::take(&mut self.buffer)   // buffer is now empty Vec, old one owned
}

// entry — the get-or-insert fight, dissolved
*counts.entry(word).or_insert(0) += 1;

// disjoint field borrows — fine; via methods on self — not
let (head, tail) = self.items.split_at_mut(1);

Related

Citations

[1] The Book, ch. 4 — Understanding Ownership [2] RFC 2094 — non-lexical lifetimes [3] The Rustonomicon — Ownership