| name | rust-syntax-borrowing |
| description | Use when the user writes code that triggers borrow-checker errors, asks "can I have & and &mut at the same time", encounters E0502 / E0499 / E0596, needs interior mutability (Cell / RefCell), splits a borrow across struct fields, or asks about reborrowing. Prevents borrow-checker fights via clone(), introduces interior mutability prematurely, or assumes NLL handles every case. Covers: `&T` (shared) vs `&mut T` (exclusive) rules, Non-Lexical Lifetimes (NLL), two-phase borrows, reborrowing, splitting borrows across struct fields and slices (`split_at_mut`), interior mutability (Cell for Copy, RefCell runtime-checked, Mutex thread-safe). Keywords: borrow, borrowing, "&", "&mut", "shared reference", "mutable reference", NLL, "non-lexical lifetimes", reborrow, "two-phase borrow", "split borrow", "split_at_mut", "interior mutability", Cell, RefCell, Mutex, E0502, E0499, E0596, "cannot borrow as mutable", "already borrowed", "multiple mutable borrows".
|
| license | MIT |
| compatibility | Designed for Claude Code. Requires Rust 1.85+, edition 2024. |
| metadata | {"author":"OpenAEC-Foundation","version":"1.0"} |
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 borrow x as mutable because it is also borrowed as immutable"), E0499 ("cannot borrow x as mutable more than once at a time"), or E0596 ("cannot borrow x as mutable, as it is not declared as mutable").
- User asks "can I have a
& and &mut at 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
&mut get 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];
println!("{r}");
v.push(4);
}
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
&mut is created but acts like an & (allows other shared reads).
- Activation phase: the
&mut becomes exclusive at the point the function call actually mutates.
fn two_phase_demo() {
let mut v = vec![1, 2, 3];
v.push(v.len());
}
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);
takes_mut(r);
r.push('?');
println!("{r}");
}
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
&mut derived from another &mut whose lifetime the compiler cannot infer.
- Stashing a
&mut into 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()
}
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.a and &mut p.b directly. Going through a helper method that returns &mut self.a collapses the borrow to the whole struct (E0499 on the next field access).
- NEVER expect this through a
Vec index expression: &mut v[0] and &mut v[1] both call IndexMut::index_mut(&mut self, _), which borrows the whole Vec. Use split_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);
let c2 = Cell::new(String::from("hi"));
let s = c2.replace(String::from("bye"));
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());
}
{
let mut v = r.borrow_mut();
v.push(4);
}
Panic scenarios:
let r = RefCell::new(0);
let _a = r.borrow();
let _b = r.borrow_mut();
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;
}
Mutex<T>: Send + Sync whenever T: Send.
- A
MutexGuard<'_, T> is !Send: the thread that locked must unlock.
- Holding a
std::sync::Mutex guard across .await is a deadlock-prone bug (Tokio runtime can move the task to another thread). Use tokio::sync::Mutex in 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
RefCell to "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.x directly).
- Using
Cell<T> for non-Copy types and expecting get() / set() semantics (it does not; you must use replace / take / swap).
- Holding
std::sync::MutexGuard across .await (use tokio::sync::Mutex).
- Returning
&mut self.field from a method that you want to call repeatedly while the borrow is live (E0499 the second time).
- Calling
as_mut() on Option<T> and then as_ref() on the same option in the same scope expecting them to coexist.
Reference links
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.