Mutable and Immutable Borrow Conflicts
A mutable and immutable borrow conflict (error[E0502]) occurs when the lifetime of an immutable borrow (&T) and the lifetime of a mutable borrow (&mut T) to the same data overlap across at least one statement.
Anatomy of an Overlapping Borrow Conflict (E0502)
For E0502 to trigger, three events must happen in sequence:
- Borrow A (
&xor&mut x) is created. - Conflicting Borrow B (
&mut xor&x) is created. - Borrow A is used again after step 2! (If Borrow A is never used after step 2, Non-Lexical Lifetimes end Borrow A before step 2 and there is no conflict.)
Overlapping Lifetimes (❌ E0502 Conflict):
let r = &data; ──┬── Shared borrow `r` starts
data.push(4); │ <-- ❌ Mutable borrow `&mut data` happens WHILE `r` is still live!
println!("{r:?}"); ──┴── Shared borrow `r` is used here!
Non-Overlapping Lifetimes (✅ Compiles Cleanly):
let r = &data; ──┬── Shared borrow `r` starts
println!("{r:?}"); ──┴── Shared borrow `r` ends here (last use)!
data.push(4); <-- ✅ Mutable borrow `&mut data` happens AFTER `r` ended!
Four Practical Patterns to Resolve E0502
1. Reorder Statements So Reads Finish Before Mutations
Move the last read of the immutable reference above the mutating call:
fn main() {
let mut log = String::from("Started");
let snapshot: &str = &log;
println!("Snapshot before edit: {snapshot}"); // Last use of `snapshot` moved ABOVE `.push_str`!
log.push_str(" -> Finished");
println!("Updated log: {log}");
}
Snapshot before edit: Started
Updated log: Started -> Finished
2. Copy or Clone the Needed Value Before Mutating
When a mutating call needs an input derived from the same struct or collection (such as vec.push(vec[0]) or s.push_str(&s.clone())), extract a Copy value or .clone() first so the read borrow ends immediately:
fn main() {
let mut history = vec![String::from("init")];
// Clone the first string in its own expression so the `&history[0]` borrow ends before `.push()`:
let first_copy: String = history[0].clone();
history.push(first_copy);
println!("history: {history:?}");
}
history: ["init", "init"]
3. Two-Phase Evaluation in Nested Method Calls (vec.push(vec.len()))
Wait—why does vec.push(vec.len()) fail to compile, while let len = vec.len(); vec.push(len); succeeds?
Because in vec.push(vec.len()), Rust evaluates the receiver &mut vec first (starting the exclusive mutable borrow of vec), and then tries to evaluate the argument vec.len() (which requires &vec) while &mut vec is already held! Binding let len = vec.len(); on the line before finishes the &vec borrow before vec.push(len) starts its &mut vec borrow:
fn main() {
let mut items = vec![10, 20, 30];
// Extract `len` first so `&items` finishes before `&mut items` in `.push()` begins:
let current_len = items.len() as i32;
items.push(current_len);
println!("items: {items:?}");
}
items: [10, 20, 30, 3]
4. Split Struct Borrows (&mut self.a and &self.b)
When a conflict occurs between &self and &mut self on a custom struct, borrow the disjoint fields directly (helper(&mut self.items, &self.config)) instead of passing &mut self.