Chapter 21: The Borrow Checker, How the Compiler Thinks
Prerequisites
You will understand
- Where borrow checking runs in the compiler pipeline
- How to simulate borrow errors mentally
- What E0382, E0502, and E0505 really mean
Reading time
45 min
+ 25 min exercises
Compiler Pipeline
Where the Borrow Checker Runs
Worksheet
How to Simulate a Borrow Error
Error Decoder Cards
What the Compiler Is Really Telling You
E0382
use of moved value
You attempted to use a binding after its ownership was transferred. The compiler
statically tracks every move and invalidates the original binding at that point.
The moved-from name still exists in the source text but has no authority.
Borrow instead of moving:
&s1. Or restructure so you use s1 before the move.
Or call .clone() if you genuinely need two independent copies.
E0502
cannot borrow as mutable because it is also borrowed as immutable
A shared reference (
&T) is alive while you try to take an exclusive reference (&mut T).
Aliasing XOR mutation: the compiler refuses to let both exist simultaneously because
the mutable borrow could invalidate what the shared reference sees.
Shorten the shared borrow's lifetime — move its last use before the mutable borrow.
NLL (Non-Lexical Lifetimes) makes this easier: a reference dies at its last use, not at scope end.
E0505
cannot move out of value because it is borrowed
A reference still points into a value you are trying to move (transfer ownership).
Moving would invalidate the reference, creating a dangling pointer — exactly what
Rust's borrow checker exists to prevent.
Ensure no references are alive at the point of the move. Restructure the code so
borrows end before ownership transfer, or use
.clone() to make the reference independent.
#![allow(unused)]
fn main() {
let mut data = String::from("hello");
let r = &data; // borrow starts
data.push_str(" world"); // E0502: &mut while &data lives
println!("{r}"); // borrow extends to here
}
Borrow starts
MIR records
r as a live shared borrow of data.
E0502: conflict
push_str requires &mut data but r holds &data. The borrow checker rejects.
NLL liveness
Borrow of
r ends at its last use (line 4), not at scope end. Moving println! above push_str would fix it.
Readiness Check - Borrow Checker Mental Simulation
| Skill | Level 0 | Level 1 | Level 2 | Level 3 |
|---|---|---|---|---|
| Trace ownership and borrows | I only react to error text | I can identify owner and references | I can mark borrow start and last-use points | I can predict likely errors before compiling |
| Decode compiler diagnostics | I copy fixes blindly | I can interpret one common error | I can map multiple errors to one root conflict | I can choose minimal structural fixes confidently |
| Restructure conflicting code | I use random clones/moves | I can fix simple overlap conflicts | I can refactor borrow scopes intentionally | I can design APIs that avoid borrow friction by construction |
Target Level 2+ before advancing into larger async/concurrency ownership scenarios.
Compiler Error Decoder - Borrow Checker Core
| Error code | What it usually means | Typical fix direction |
|---|---|---|
| E0502 | Shared and mutable borrow overlap | End shared borrow earlier or split scope before mutable operation |
| E0505 | Move attempted while borrowed | Reorder to end borrow first, or clone if independent ownership is required |
| E0515 | Returning reference to local/temporary data | Return owned value or borrow from caller-provided input |
Use one worksheet for every failure: owner, borrow region, conflicting operation, smallest safe rewrite.