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
40 min
+ 25 min exercises
Builds on Chapters 10 and 11
This chapter formalizes what Ch 10 introduced informally. The aliasing-XOR-mutation rule is the reason Ch 10's "one owner" works: shared mutation would mean two owners.
Revisit Ch 10 →
You'll need this for Chapters 21 and 32
The borrow checker (Ch 21) enforces these rules at MIR level. Send/Sync (Ch 32) extend aliasing-XOR-mutation to thread boundaries.
Ch 21: Borrow Checker →
Aliasing Problem
Two Readers Is Stable, Reader Plus Writer Is Not
Iterator Safety
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
}
Owner created
v owns the Vec. Heap buffer at address A.
&T borrow
first borrows into v's buffer. It assumes buffer stability.
E0502
push needs &mut v but first's &v is still live. push may reallocate, moving the buffer.
Dangling prevented
If
push reallocated, first would point to freed memory. Borrow checker prevents it.
In Your Language: Iterator Invalidation
Rust — compile-time prevention
#![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
}
Python — runtime crash possible
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
Step 1: Vec allocates
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.
Step 2: Borrow into the buffer
let first = &v[0]; — first is a &i32 pointing directly into the heap buffer. It assumes the buffer is at a stable address.
Step 3: Push may reallocate
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.
Step 4: Rust prevents it
The borrow checker sees that
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.
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.