References and Borrowing

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:

  1. Borrow A (&x or &mut x) is created.
  2. Conflicting Borrow B (&mut x or &x) is created.
  3. 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.