Frontend Labs
⋯

7. Borrowing: shared XOR mutable

Moving values everywhere would be impractical. Usually you want to let code use a value without taking ownership. That is a borrow, written as a reference.

7.1 The rule

At any moment, a value can have either:

  • any number of shared references &T (read-only), or
  • exactly one mutable reference &mut T (read-write),

and no reference may outlive the value it points to.

fn total_len(words: &[String]) -> usize {      // borrows; the caller keeps ownership
    words.iter().map(|w| w.len()).sum()
}

fn shout(words: &mut Vec<String>) {           // exclusive borrow; may modify
    for w in words.iter_mut() {
        *w = w.to_uppercase();
    }
    words.push("!".to_string());
}

fn main() {
    let mut words = vec!["hello".to_string(), "world".to_string()];
    let n = total_len(&words);   // shared borrow, ends when the call returns
    shout(&mut words);           // mutable borrow, ends when the call returns
    println!("{n} {words:?}");   // owner still valid: ["HELLO", "WORLD", "!"]
}

7.2 Why the rule exists: iterator invalidation

// DOES NOT COMPILE
fn main() {
    let mut v = vec![1, 2, 3];
    let first = &v[0];       // shared borrow into v's heap buffer
    v.push(4);               // needs &mut v, and push may REALLOCATE the buffer
    println!("{first}");     // first would point into freed memory
}
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
  |     let first = &v[0];
  |                  - immutable borrow occurs here
  |     v.push(4);
  |     ^^^^^^^^^ mutable borrow occurs here
  |     println!("{first}");
  |               ------- immutable borrow later used here

This is the same bug as Go's append aliasing trap and C's realloc-then-dangling-pointer bug. In Go it silently corrupts data. In C it is UB. In Rust it does not compile.

"Shared XOR mutable" rules out a whole category of bugs at once:

Bug Why it cannot happen
Iterator invalidation you cannot mutate a collection while a borrow of it is alive
Use-after-free via a reference a reference cannot outlive its owner
Data races a data race needs two threads with access, at least one writing; that is shared AND mutable at once (Ch 28)
Aliasing surprises for the optimizer &mut T is guaranteed unique, so LLVM gets noalias information C only gets from restrict

7.3 Borrows last until their last use (non-lexical lifetimes)

fn main() {
    let mut scores = vec![10, 20, 30];
    let max = scores.iter().max().copied();   // `.copied()` turns Option<&i32> into Option<i32>
    // The shared borrow from iter() ended at the end of the line above,
    // because `max` holds a copy, not a reference.
    scores.push(40);
    println!("{max:?} {scores:?}");

    let first = &scores[0];
    println!("{first}");       // last use of `first`: the borrow ends here
    scores.push(50);           // so this is fine
}

Most of the friction with the borrow checker goes away once you remember that a borrow ends at its last use, not at the end of the block.

7.4 Dereferencing and auto-ref

fn bump(n: &mut i32) {
    *n += 1;                       // `*` dereferences: write through the reference
}

fn main() {
    let mut count = 0;
    bump(&mut count);
    let s = String::from("abc");
    let r = &s;
    println!("{} {}", r.len(), (*r).len()); // method calls auto-dereference: r.len() just works
    println!("{count}");
}

The . operator adds &, &mut, or * as needed to make a method call type-check. That is why you rarely write * outside assignments.

7.5 The four ways to fix a borrow-checker error

When you hit E0499, E0502, or E0505, one of these almost always works:

  1. Shorten the borrow. Copy or clone the small value you need (let id = user.id;) before the mutation.
  2. Reorder. Finish reading before you start writing.
  3. Split the borrow. Borrow disjoint fields (let (a, b) = (&mut s.left, &mut s.right);) or slices (v.split_at_mut(mid)).
  4. Restructure ownership. Use indices instead of references (Ch 27), an entry API (Ch 11), or move the data instead of borrowing it.

Wrapping everything in Rc<RefCell<T>> also works, but it moves the checks to runtime. Treat it as a last resort (Ch 22, 68).


Generated from real-life-rust-guide.md · 8 Oct 2026 16:36