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, noDrop— 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::dropis 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:
&Tshared/read-only,&mut Texclusive. The exclusivity of&mutis what makes iterator invalidation, torn updates and (later, across threads) data races impossible — and it's why the compiler can optimize aggressively aroundnoaliasguarantees. - 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:
&mutimplicitly 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.leftand&mut self.rightcoexist), and since edition 2021 closures capture individual fields rather than whole structs — but method calls borrow all ofself, 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 aStringon 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
- Ownership, memory & concurrency — the model — parent map of the group.
- Lifetimes — naming how long these borrows live, at API boundaries.
- Memory without GC —
Rc, arenas and interior mutability: the escape valves the fights table reaches for. - Fearless concurrency — the same rules crossing thread boundaries.
- miri — enforcing these rules dynamically where
unsafeopts out. - The Rust Programming Language — chapter 4 is the canonical first pass on everything here.
Citations
[1] The Book, ch. 4 — Understanding Ownership [2] RFC 2094 — non-lexical lifetimes [3] The Rustonomicon — Ownership