Chapter 17: Borrowing, Constrained Access
Prerequisites
You will understand
- Aliasing XOR mutation as a formal invariant
- Why iterator invalidation is impossible in Rust
- How NLL changed Rust 2018 borrow scoping
Reading time
Two Readers Is Stable, Reader Plus Writer Is Not
Why Rust Rejects Iterator Invalidation
#![allow(unused)]
fn main() {
let mut v = vec![1, 2, 3];
let first = &v[0]; // shared borrow of v
v.push(4); // ERROR: &mut borrow while &v lives
println!("{first}"); // shared borrow used after conflict
}
v owns the Vec. Heap buffer at address A.
first borrows into v's buffer. It assumes buffer stability.
push needs &mut v but first's &v is still live. push may reallocate, moving the buffer.
push reallocated, first would point to freed memory. Borrow checker prevents it.
In Your Language: Iterator Invalidation
#![allow(unused)]
fn main() {
let mut v = vec![1, 2, 3];
let first = &v[0];
v.push(4); // COMPILE ERROR
println!("{first}"); // borrow still live
}
v = [1, 2, 3]
it = iter(v)
next(it) # 1
v.append(4) # works! but...
# iterator may give unexpected results
# no compile-time guard
Walk Through: Why push Invalidates a Reference
let mut v = vec![1, 2, 3]; — Vec allocates a heap buffer large enough for 3 elements (capacity may be 3 or 4). v owns this buffer.
let first = &v[0]; — first is a &i32 pointing directly into the heap buffer. It assumes the buffer is at a stable address.
v.push(4); — If capacity is exhausted, Vec allocates a new, larger buffer, copies all elements, and frees the old one. first now points to freed memory → dangling pointer.
first holds &v (shared borrow) while push requires &mut v (exclusive). Since `first` is used after `push`, their borrow regions overlap → E0502 at compile time. No runtime crash possible.
Now run that exact program through the simulator below. Watch the shared borrow point straight into the heap buffer, then watch the compiler refuse the mutation while that borrow is still alive.
Interactive simulation (requires JavaScript): a shared borrow into a Vec's heap buffer freezes the Vec; calling push while the borrow is alive fails with E0502 because reallocation would leave the reference dangling. Under non-lexical lifetimes the borrow ends at its last use, so removing the final use makes the mutation legal.
Readiness Check - Borrowing Confidence
Before proceeding, self-check your ability to reason about aliasing and mutation.
| Skill | Level 0 | Level 1 | Level 2 | Level 3 |
|---|---|---|---|---|
| Explain aliasing XOR mutation | I memorize the phrase only | I can explain many-readers/one-writer | I can identify why a specific borrow conflict occurs | I can predict borrow regions before compiling |
| Debug borrow conflicts | I try random edits | I can fix one obvious E0502 case | I can choose between borrow narrowing and ownership transfer | I can refactor APIs to make borrow discipline obvious |
| Design mutation flow safely | I mutate where convenient | I can isolate mutation blocks | I can structure code to minimize overlapping borrows | I can review code for hidden iterator invalidation risks |
Target Level 2+ before moving to Chapter 21.
Compiler Error Decoder - Constrained Access
| Error code | What it usually means | Typical fix direction |
|---|---|---|
| E0502 | Immutable and mutable borrows overlap | Narrow borrow lifetimes with smaller scopes and earlier last-use |
| E0499 | Two mutable borrows coexist | Refactor into one mutation path at a time |
| E0506 | Assigned to a value while it was still borrowed | Delay assignment until borrow ends or clone required data first |
Always ask: “Which borrow must stay live here?” Then eliminate or shorten the other one.