Why this matters
The borrow checker is the part of rustc that enforces the borrowing rules. Beginners often describe learning Rust as "fighting the borrow checker". The fight gets much shorter once you know how it thinks: what it tracks, which questions it asks, and which handful of restructurings resolve nearly every error. This lesson is that model.
What the checker tracks
For each reference, the checker computes a region: the set of points in the program where the reference may still be used. A region starts where the borrow is created and ends at the reference's last use. Then, at every point in the program, it asks one question:
Is any action here incompatible with a borrow whose region includes this point?
The incompatible actions, for a value x that is borrowed:
borrow live on x | forbidden on x (except through that borrow) |
|---|---|
shared &x | mutating, mutably borrowing, moving, dropping |
mutable &mut x | everything: reading, borrowing, mutating, moving, dropping |
That is the whole model. Let us annotate a program with regions:
fn main() {
let mut s = String::from("hi");
let r = &s; // r's region starts
println!("{r}"); // last use of r: region ends here
s.push('!'); // mutation: fine, no borrow is live
let w = &mut s; // w's region starts
w.push('?'); // last use of w: region ends
println!("{s}"); // reading s: fine
}
hi
hi!?
line: 3 4 5 6 7 8
r: [======] shared borrow of s
w: [=====] mutable borrow of s
s.push: X needs &mut s: no borrow live, ok
println s: X reads s: no borrow live, ok
Regions are not scopes
Before 2018, Rust used the variable's scope (to the closing brace) as the region, and many reasonable programs were rejected. Today's analysis, non-lexical lifetimes (NLL), uses actual uses. A reference variable may stay in scope long after its last use; that does not matter.
But a later use extends the region backwards over everything in between:
fn main() {
let mut s = String::from("hi");
let r = &s;
s.push('!');
println!("{r}");
}
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
--> main.rs:4:5
|
3 | let r = &s;
| -- immutable borrow occurs here
4 | s.push('!');
| ^^^^^^^^^^^ mutable borrow occurs here
5 | println!("{r}");
| --- immutable borrow later used here
The fix is to move the use of r before the mutation, or to create r after it.
E0506: assigning to something borrowed
Assignment is mutation too:
fn main() {
let mut scores = [10, 20, 30];
let best = &scores[2];
scores[2] = 0;
println!("{best}");
}
error[E0506]: cannot assign to `scores[_]` because it is borrowed
--> main.rs:4:5
|
3 | let best = &scores[2];
| ---------- `scores[_]` is borrowed here
4 | scores[2] = 0;
| ^^^^^^^^^^^^^ `scores[_]` is assigned to here but it was already borrowed
5 | println!("{best}");
| ------ borrow later used here
Note that the checker treats the whole array as borrowed when you borrow one element through indexing; it does not try to prove which index is which. Here the fix is simplest of all: i32 is Copy, so store the value, not a reference: let best = scores[2];.
What the checker can see precisely
The checker understands disjoint fields. Borrowing two different fields of a tuple (or, later, a struct) mutably at the same time is fine:
fn main() {
let mut pair = (String::from("left"), String::from("right"));
let a = &mut pair.0;
let b = &mut pair.1;
a.push('!');
b.push('?');
println!("{:?}", pair);
}
("left!", "right?")
It does not see through indexes, because index values are only known at run time:
fn main() {
let mut arr = [1, 2, 3];
let a = &mut arr[0];
let b = &mut arr[1];
*a += 10;
*b += 10;
}
error[E0499]: cannot borrow `arr[_]` as mutable more than once at a time
--> main.rs:4:13
|
3 | let a = &mut arr[0];
| ----------- first mutable borrow occurs here
4 | let b = &mut arr[1];
| ^^^^^^^^^^^ second mutable borrow occurs here
5 | *a += 10;
| -------- first borrow later used here
|
= help: use `.split_at_mut(position)` to obtain two mutable non-overlapping sub-slices
The help is accurate: the standard library has split_at_mut, which splits one mutable slice into two non-overlapping ones (lesson 5 and module 15 show how it works inside).
It also understands that method call arguments run before the call:
fn main() {
let mut v = vec![1, 2, 3];
v.push(v.len());
println!("{:?}", v);
}
[1, 2, 3, 3]
push needs &mut v, and v.len() needs &v, in the same expression. This "two-phase borrow" is allowed because the mutable borrow only becomes active after the argument is computed.
A toolbox for borrow errors
When the checker says no, one of these almost always works. In rough order of preference:
- Reorder. Finish using the first borrow before starting the conflicting action. Often the whole fix is moving one line.
- Copy the value out. For Copy types, take the value (
let x = v[i];) instead of a reference. You hold no borrow at all. - Use indexes instead of references. Store "the position of the best element" (
usize) rather than a reference to it, and index when needed. Indexes are plain numbers and borrow nothing. - Shorten the borrow with a block. Put the code using the reference in
{ }so its last use is clearly before the conflict. - Split the data. Borrow disjoint fields, or use
split_at_mutfor slices. - Clone. When you really need an independent copy,
cloneremoves the relationship entirely, at the cost of copying.
Later modules add tools for genuinely shared, mutable data (Rc<RefCell<T>>, Arc<Mutex<T>>) and lifetimes for references stored in structs. But for code within one function, the list above covers almost everything.
Here is strategy 3 in action, a classic "update while scanning" situation:
fn main() {
let mut names = vec![String::from("bo"), String::from("alexandra"), String::from("cy")];
// remember WHERE the longest is, not a reference to it
let mut longest = 0;
for i in 1..names.len() {
if names[i].len() > names[longest].len() {
longest = i;
}
}
names[longest].push_str(" (longest)");
names.push(String::from("dee"));
println!("{:?}", names);
}
["bo", "alexandra (longest)", "cy", "dee"]
A &String to the longest name could not coexist with the push_str and push calls; an index can.
Why the checker is conservative
The checker sometimes rejects programs that would actually be fine at run time. It must decide at compile time, from the code alone, without running it, and it must never accept an unsafe program. So when in doubt it says no. In practice the rejections are usually telling you something real about your design: two parts of the code both think they can change the same data. Restructuring to make ownership clear tends to make the code better, not just accepted.
Common mistakes
- Reading only the headline. The labels "borrow occurs here", "later used here" are the information you need: they mark both ends of the conflicting region.
- Fighting with clones everywhere. Strategies 1 to 3 are free and usually sufficient.
- Holding references into a collection while changing the collection. Store indexes or copy values.
Recap
- The checker computes, for each borrow, the region from creation to last use (NLL).
- In that region, a shared borrow forbids mutation, moves and drops of the value; a mutable borrow forbids every other access.
- It sees disjoint tuple and struct fields, but not different indexes into the same array or vector.
- Method-call arguments are evaluated before the call's own mutable borrow activates.
- Fixes: reorder, copy out, use indexes, shorten with a block, split, and only then clone.
Going further: take any E0502 you hit in the exercises and draw the regions as in the table above. The blog post "Non-lexical lifetimes: introduction" by Niko Matsakis explains the history of this analysis.