rust-syntax-borrowing
Mechanics of &T / &mut T, Non-Lexical Lifetimes, two-phase borrows, reborrowing, split borrows, and interior mutability. The borrow checker is not your enemy: most errors point to a real soundness issue that .clone() would only paper over.
Cross-references: [[rust-syntax-ownership]] (move semantics is the alternative to borrowing), [[rust-syntax-lifetimes]] (lifetime annotations on borrows), [[rust-errors-borrow-checker]] (E-code-specific decoding), [[rust-core-memory-model]] (conceptual overview).
When to use this skill
- User writes code that fails with
E0502("cannot borrowxas mutable because it is also borrowed as immutable"),E0499("cannot borrowxas mutable more than once at a time"), orE0596("cannot borrowxas mutable, as it is not declared as mutable"). - User asks "can I have a
&and&mutat the same time". - User reaches for
.clone()to silence the borrow checker. - User asks "what is
RefCell/Cell/ interior mutability". - User wants to mutate two fields of a struct from two threads / two closures / two scopes.
- User asks "how do I get two mutable references into different parts of a
Vec". - User asks "what is reborrowing" or "why does my
&mutget passed but the original keeps working".
The borrow rules (verbatim from the Rust Book)
At any given time, you can have either one mutable reference or any number of immutable references. References must always be valid.
ALWAYS state this rule first when explaining borrow-checker errors. It is the only rule you ever need; everything else (NLL, two-phase, split borrows) is the compiler being smart about when this rule is checked.
Encoded as a truth table:
Active reference set on x |
Allowed? | Why |
|---|---|---|
| zero refs | yes | No aliasing concern. |
N &x (N >= 1), no &mut x |
yes | Shared reads are race-free. |
exactly one &mut x, no &x |
yes | Unique writer, no aliasing. |
any &x + any &mut x simultaneously |
NO | E0502 / E0499. |
two &mut x simultaneously |
NO | E0499. |
Quick reference table
| Concept | One-line rule |
|---|---|
&T |
Shared (immutable) reference. Many allowed. Copy. |
&mut T |
Exclusive (mutable) reference. Exactly one allowed. Not Copy. |
| NLL | A borrow ends at its last use, not at end of scope. |
| Two-phase borrow | A &mut is split into a reservation phase (acts like &) and an activation phase. Allows vec.push(vec.len()). |
| Reborrow | &mut *r (often implicit). Creates a new borrow that ends when used; the original &mut remains live afterwards. |
| Split borrow (struct) | Borrowing two distinct fields of &mut self is allowed. The compiler tracks disjoint paths. |
| Split borrow (slice) | slice.split_at_mut(i) returns (&mut [T], &mut [T]) over disjoint halves. |
Cell<T> |
Interior mutability for Copy types or move in/out. No runtime cost. !Sync. |
RefCell<T> |
Interior mutability with runtime borrow checking. Panics on violation. !Sync. |
Mutex<T> |
Thread-safe interior mutability. Blocks on contention. Send + Sync if T: Send. |
NLL: borrows end at last use
Since Rust 2018 (and improved in every release since), borrows end at their last use, not at the closing brace. This is called Non-Lexical Lifetimes.
fn nll_demo() {
let mut v = vec![1, 2, 3];
let r = &v[0]; // immutable borrow starts
println!("{r}"); // immutable borrow ends here (last use of `r`)
v.push(4); // OK: no live borrow at this point
}
Pre-NLL (Rust 2015 lexical) the immutable borrow would have lasted to the } and v.push(4) would fail with E0502.
ALWAYS look at the last use of a borrow before claiming the borrow checker is wrong. If r is never read after a certain point, the borrow is dead from that point on.
NEVER assume the borrow is alive for the whole {} block. That is the pre-NLL mental model and is wrong on edition 2018+.
Two-phase borrows: why vec.push(vec.len()) compiles
A &mut borrow is split into two phases by the compiler:
- Reservation phase: the
&mutis created but acts like an&(allows other shared reads). - Activation phase: the
&mutbecomes exclusive at the point the function call actually mutates.
fn two_phase_demo() {
let mut v = vec![1, 2, 3];
v.push(v.len()); // OK: reservation of &mut v starts at call,
// v.len() reads v as &v before activation
}
Without two-phase borrows this code would fail with E0502 ("cannot borrow v as immutable because it is also borrowed as mutable"). Two-phase exists only for method-call argument expressions. It does NOT help in patterns like let x = &mut v; v.len(); (E0502 there).
NEVER rely on two-phase outside method-call argument position. If you need shared+exclusive access in arbitrary order, restructure the code.
Reborrowing: passing a &mut without moving it
A reborrow creates a new &mut (or &) borrow from an existing one. Syntactically: &mut *r. Most reborrows are implicit: when you pass a &mut T to a function that takes &mut T, the compiler reborrows for you.
fn takes_mut(s: &mut String) { s.push('!'); }
fn reborrow_demo() {
let mut s = String::from("hi");
let r: &mut String = &mut s;
takes_mut(r); // implicit reborrow: takes_mut gets &mut *r
takes_mut(r); // original `r` still usable
r.push('?');
println!("{r}"); // "hi!!?"
}
ALWAYS think of method calls on a &mut T as receiving an implicit reborrow. The original &mut remains live for use after the call.
Explicit reborrow is needed when:
- Returning a
&mutderived from another&mutwhose lifetime the compiler cannot infer. - Stashing a
&mutinto a generic context that does not perform auto-reborrow (some closure / trait-object cases).
fn get_mut<'a>(opt: &'a mut Option<String>) -> Option<&'a mut String> {
opt.as_mut() // as_mut() already reborrows; nothing extra needed
}
Split borrows: different fields, different references
The compiler tracks borrow paths at the field level. You can borrow disjoint fields of &mut self independently:
struct Pair { a: String, b: String }
fn split_struct(p: &mut Pair) {
let ra = &mut p.a;
let rb = &mut p.b;
ra.push('!');
rb.push('?');
}
This compiles because p.a and p.b are disjoint paths. However:
- ALWAYS write the borrows as
&mut p.aand&mut p.bdirectly. Going through a helper method that returns&mut self.acollapses the borrow to the whole struct (E0499 on the next field access). - NEVER expect this through a
Vecindex expression:&mut v[0]and&mut v[1]both callIndexMut::index_mut(&mut self, _), which borrows the wholeVec. Usesplit_at_mut(below) instead.
For slice / Vec access:
fn split_slice(v: &mut [i32]) {
let (left, right) = v.split_at_mut(1);
left[0] = 10;
right[0] = 20;
}
split_at_mut(idx) returns two non-overlapping &mut [T] halves and is the standard pattern for two-mutable-pieces-of-one-Vec. For more partitions use chunks_mut, split_first_mut, iter_mut.
Interior mutability: when "&T but mutate" is genuinely needed
Interior mutability is the sound escape hatch that lets you mutate through a shared reference. All sound primitives are built on UnsafeCell<T>, the only type the compiler trusts to allow this.
Pick interior mutability ONLY when you have proven that:
- The borrow rules cannot be expressed at compile-time for this design (typical: observer pattern, shared cache, callback registries).
- A simpler restructure (split borrow, NLL-friendly scoping, returning indices instead of references) does not work.
Cell: zero-cost, Copy (or move in/out)
Cell<T> does not hand out references to its contents. You can only move values in and out, or read by copy if T: Copy. No runtime check, no overhead.
use std::cell::Cell;
let c = Cell::new(0_i32);
c.set(c.get() + 1); // T: Copy path
let c2 = Cell::new(String::from("hi"));
let s = c2.replace(String::from("bye")); // move out, put new in
ALWAYS prefer Cell over RefCell when T: Copy or when you only need swap semantics. Cell has zero runtime cost.
NEVER try to take &T or &mut T out of a Cell<T>: the API is intentionally designed not to expose them.
RefCell: runtime-checked borrows
RefCell<T> hands out Ref<'_, T> and RefMut<'_, T> smart pointers and tracks the borrow state at runtime. Panics if the borrow rules are violated.
use std::cell::RefCell;
let r = RefCell::new(vec![1, 2, 3]);
{
let v = r.borrow();
println!("{}", v.len()); // immutable borrow, OK
} // borrow ends here
{
let mut v = r.borrow_mut();
v.push(4); // exclusive borrow, OK
} // borrow ends here
Panic scenarios:
let r = RefCell::new(0);
let _a = r.borrow();
let _b = r.borrow_mut(); // PANIC at runtime: already borrowed
The borrow tokens released when Ref / RefMut drop. NEVER hold a RefCell borrow across a function call that might re-enter and borrow the same cell: you will panic.
ALWAYS use try_borrow() / try_borrow_mut() in code paths where re-entry is plausible.
Mutex: thread-safe equivalent of RefCell
Mutex<T> provides interior mutability across threads. Acquiring the lock blocks until the previous holder releases it.
use std::sync::Mutex;
let m = Mutex::new(0_u32);
{
let mut guard = m.lock().unwrap();
*guard += 1;
} // guard drops, lock released
Mutex<T>: Send + SyncwheneverT: Send.- A
MutexGuard<'_, T>is!Send: the thread that locked must unlock. - Holding a
std::sync::Mutexguard across.awaitis a deadlock-prone bug (Tokio runtime can move the task to another thread). Usetokio::sync::Mutexin async code. See [[rust-errors-async]].
NEVER use std::sync::Mutex in async code that holds the guard across .await. ALWAYS use tokio::sync::Mutex there.
Decision tree: which reference / mutability to use
Does the callee need to OWN the value (consume, store, or transfer it)?
|
+-- Yes: take T (owned). Caller is moving in.
|
+-- No: does the callee MUTATE?
|
+-- Yes: take &mut T. (Only one caller at a time.)
| |
| +-- Need shared writes? Move to one of:
| * Cell<T> if T: Copy or swap-only semantics, single-thread.
| * RefCell<T> if runtime borrow tracking is acceptable, single-thread.
| * Mutex<T> if multi-thread / Sync needed.
| * AtomicX if T is a primitive integer/bool/pointer.
|
+-- No: take &T. (Many callers welcome.)
|
+-- T is `String`? Take &str instead.
+-- T is `Vec<X>`? Take &[X] instead.
+-- T is `Box<dyn X>`? Take &dyn X instead.
+-- T is `PathBuf`? Take &Path instead.
ALWAYS take the most flexible unsized borrow (&str, &[T], &Path, &dyn X) over the owned reference (&String, &Vec<T>, &PathBuf, &Box<dyn X>). Clippy enforces this via ptr_arg.
Common error decoder
| Code | Plain English | First thing to try |
|---|---|---|
| E0502 | "borrowed as mutable while also borrowed as immutable" | Shorten the immutable borrow scope; rely on NLL. |
| E0499 | "borrowed as mutable more than once" | Use split borrows, split_at_mut, or restructure into two scopes. |
| E0596 | "cannot borrow as mutable, not declared mut" | Add mut to the binding, or take &mut instead of &. |
| E0507 | "cannot move out of borrowed content" | Match &pattern to bind by reference, or use std::mem::replace. |
| E0382 | "borrow of moved value" | The value was moved; clone or pass by reference upstream. |
For each E-code see [[rust-errors-borrow-checker]] for full decoding.
Avoid these mistakes
(Full list with WHYs in references/anti-patterns.md.)
- Reaching for
.clone()to silence E0502 instead of letting NLL end the borrow. - Using
RefCellto "work around" the borrow checker when a scope restructure would suffice. - Holding a
RefCell::borrow()across a function call that may recursively borrow. - Trying
split_at_mut-like tricks on arbitrary struct fields (use destructuring or&mut self.xdirectly). - Using
Cell<T>for non-Copytypes and expectingget()/set()semantics (it does not; you must usereplace/take/swap). - Holding
std::sync::MutexGuardacross.await(usetokio::sync::Mutex). - Returning
&mut self.fieldfrom a method that you want to call repeatedly while the borrow is live (E0499 the second time). - Calling
as_mut()onOption<T>and thenas_ref()on the same option in the same scope expecting them to coexist.
Reference links
- Rust Book Ch 4.2: References and Borrowing
std::celloverviewstd::cell::Cellstd::cell::RefCellstd::cell::UnsafeCellstd::sync::Mutexslice::split_at_mut- Rustonomicon: Aliasing
For deeper drill-downs see:
references/methods.md: complete API signatures for&T,&mut T,Cell,RefCell,Mutex, slice splitters.references/examples.md: working code examples for NLL, two-phase, reborrow, split borrows, interior mutability.references/anti-patterns.md: 10+ documented anti-patterns with WHY and FIX.