Beginner · Borrowing, slices and lifetimes · topic 15 of 50 · 35 min
The borrow checker in practice
Explanation
What the borrow checker does
The borrow checker reads your code and tracks every reference: where it is created, where it is last used, and what it points to. If any path breaks the rules, the program does not compile. This feels strict at first, but each error it reports is a real bug you would otherwise find later, maybe in production.
A borrow ends at its last use
Modern Rust uses non-lexical lifetimes (NLL): a reference is "alive" only from where it is created to where it is last used. It does not have to wait for the closing brace. That is why this code is fine:
let mut list = vec![1, 2, 3];
let first = &list[0]; // shared borrow starts
println!("{first}"); // ...and ends here (last use)
list.push(4); // mutable borrow: OK, no shared borrow is aliveE0382: use of a moved value
let s = String::from("hi");
let t = s;
println!("{s}");
// error[E0382]: borrow of moved value: `s`E0499: two mutable borrows at once
let mut name = String::from("Ana");
let a = &mut name;
let b = &mut name;
a.push('!');
// error[E0499]: cannot borrow `name` as mutable more than once at a timelet mut name = String::from("Ana");
{
let a = &mut name;
a.push('!');
} // a is done
let b = &mut name;
b.push('?');
println!("{name}"); // Ana!?E0502: mutable borrow while a shared borrow is still used
let mut v = vec![10, 20, 30];
let first = &v[0];
v.push(40);
println!("{first}");
// error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutableWhy is this dangerous? push may need more capacity. Then the Vec allocates a bigger block of heap memory, copies the items and frees the old block. first would still point to the old, freed block: a dangling reference. In C++ this exact bug (iterator invalidation) is common. Fix: copy the value out before pushing (let first = v[0]; works because i32 is Copy), or use first before the push.
before push: v.ptr --> [10][20][30] first --> [10] (old block)
after push: v.ptr --> [10][20][30][40] old block FREED, first would dangleE0106 and E0597: references that would dangle
fn make() -> &String { // error[E0106]: missing lifetime specifier
let s = String::from("temp");
&s // s is dropped at the end of make
}let r;
{
let x = 5;
r = &x;
} // x is dropped here
println!("{r}");
// error[E0597]: `x` does not live long enoughA strategy list for borrow errors
- Read the whole error. It shows where each borrow starts and where it is used later.
- Shorten the borrow: use the reference earlier, or put it in a smaller block.
- Copy or clone small values out instead of holding a reference (let price = cart[0];).
- Restructure: do all the reading first, collect what you need, then do the writing.
- Store indexes instead of references when you need to remember a position in a Vec.
- If the function returns a reference to local data, return the owned value instead.
Need help?
Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.
Try:
Watch the lesson
The borrow checker in practice · 1/7
The borrow checker is the part of the compiler that enforces the borrowing rules. Every error it shows is a real bug avoided.
Prefer a video by a teacher? Find free videos on “rust borrow checker errors explained E0502 E0499” on YouTube ↗
In real life: Changing a list while looping over it
A developer at a logistics company wants to add a follow-up delivery for every delayed parcel while looping over the parcels list. In many languages this either crashes or silently skips items. Rust refuses to compile, because the loop holds a shared borrow while push needs a mutable one. The fix is to collect the new parcels first and add them after the loop.
fn main() {
let mut parcels = vec![("P1", true), ("P2", false), ("P3", true)];
let mut follow_ups = Vec::new();
for (id, delayed) in &parcels {
if *delayed {
follow_ups.push((*id, false));
}
}
parcels.extend(follow_ups); // mutable borrow after the loop: fine
println!("{} parcels", parcels.len());
}Practice (0/3 done)
Does this compile? let mut s = String::from("a"); let r1 = &s; let r2 = &s; println!("{r1} {r2}"); let r3 = &mut s; r3.push('b');
Which line causes the error? 1: let mut v = vec![1, 2, 3]; 2: let last = v.last().unwrap(); 3: v.push(4); 4: println!("{last}");
Type the E0502 example (borrow v[0], push, then print) into a project and read the full compiler message. Then fix it in two ways: (1) copy the number out with let first = v[0]; (2) move the println! before the push. Write one sentence on why the original was dangerous.
Quick quiz
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Slices and stringsBorrowing, slices and lifetimes · same module
- LifetimesBorrowing, slices and lifetimes · same module
- Interview questions: ownership, borrowing and lifetimesRust careers and interviews
- Rc, RefCell and WeakClosures, iterators and smart pointers
- Unsafe Rust: what it is and is notTesting, tooling and unsafe Rust
- Vec<T>: growable listsError handling and collections
Questions people ask about this
Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.
Until when is a shared borrow alive under non-lexical lifetimes?
Answer: Until its last use
A borrow lasts from creation to its last use.
What does E0499 mean?
Answer: Two mutable borrows of the same value overlap
E0499: cannot borrow as mutable more than once at a time.
Why is holding &v[0] across v.push(...) dangerous?
Answer: push may reallocate the heap buffer, leaving the reference dangling
Reallocation frees the old buffer, which the reference would still point to.
A function returns a reference to a local String. What is the best fix?
Answer: Return the String itself (owned)
The local is dropped at the end of the function, so return ownership instead.
Which is NOT a good way to fix borrow errors?
Answer: Wrap everything in unsafe
unsafe does not turn off the borrow checker for references and hides real bugs.
What is borrowing?Related
Answer: Using a value through a reference without taking ownership
A reference borrows: the owner keeps the value.
Which statement about non-lexical lifetimes is correct?Related
Answer: Borrows last until their last use
NLL ends borrows at their last use.
From Interview questions: ownership, borrowing and lifetimes
The compiler suggests adding 'static to a returned reference to a local String. What is usually the right fix?Related
Answer: Return the owned String instead
A local String is dropped at the end of the function; returning ownership is the clean fix.
From Lifetimes
Which type should you use for shared ownership across threads?Related
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.
Next steps
- 2 more topics in “Borrowing, slices and lifetimes” before its exam: Slices and strings, Lifetimes.
- Module 4 exam: Borrowing, slices and lifetimes · 13 questions · 25 min, without help. 4 topics of this module still to finish first.
- Then the next module, Structs, enums and pattern matching. 50 topics left in the course before the final exam.