Skip to content

Beginner · Borrowing, slices and lifetimes · topic 15 of 50 · 35 min

The borrow checker in practice

Explanation

The borrow checker is the part of the compiler that enforces the borrowing rules. A borrow lasts only until its last use, not to the end of the block. Learn the common errors: E0382 (use after move), E0499 (two mutable borrows), E0502 (mutable borrow while a shared one is still used), E0106 (missing lifetime) and E0597 (does not live long enough), and the standard fixes.

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:

rust
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 alive
Compiles. Move the println! after the push and it fails (see E0502 below).

E0382: use of a moved value

output
let s = String::from("hi");
let t = s;
println!("{s}");
// error[E0382]: borrow of moved value: `s`
Fix: use t; or borrow (let t = &s;); or clone (let t = s.clone();).

E0499: two mutable borrows at once

output
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 time
Fix: finish with a before creating b, so the borrows do not overlap.
rust
let mut name = String::from("Ana");
{
    let a = &mut name;
    a.push('!');
}                       // a is done
let b = &mut name;
b.push('?');
println!("{name}");     // Ana!?
Fixed: the two mutable borrows no longer overlap.

E0502: mutable borrow while a shared borrow is still used

output
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 immutable
This one protects you from a real crash, explained below.

Why 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.

output
before push:  v.ptr --> [10][20][30]          first --> [10] (old block)
after push:   v.ptr --> [10][20][30][40]      old block FREED, first would dangle
Rust refuses to compile instead of letting first dangle.

E0106 and E0597: references that would dangle

output
fn make() -> &String {          // error[E0106]: missing lifetime specifier
    let s = String::from("temp");
    &s                          // s is dropped at the end of make
}
Fix: return the String itself (fn make() -> String), moving ownership out.
output
let r;
{
    let x = 5;
    r = &x;
}                               // x is dropped here
println!("{r}");
// error[E0597]: `x` does not live long enough
r would point to memory that is gone. Fix: make x live as long as r (declare it outside the block).

A strategy list for borrow errors

  1. Read the whole error. It shows where each borrow starts and where it is used later.
  2. Shorten the borrow: use the reference earlier, or put it in a smaller block.
  3. Copy or clone small values out instead of holding a reference (let price = cart[0];).
  4. Restructure: do all the reading first, collect what you need, then do the writing.
  5. Store indexes instead of references when you need to remember a position in a Vec.
  6. If the function returns a reference to local data, return the owned value instead.
Careful: Do not reach for unsafe or random clones to "beat" the borrow checker. Almost always, the error is pointing at a real design problem with a clean fix.

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

Borrow checker = bug finder

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.

rust
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());
}
Prints: 5 parcels

Practice (0/3 done)

Task 1 · Question

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');

Task 2 · Question

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}");

Task 3 · Practice

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

Question 1

1.Until when is a shared borrow alive under non-lexical lifetimes?

Question 2

2.What does E0499 mean?

Question 3

3.Why is holding &v[0] across v.push(...) dangerous?

Question 4

4.A function returns a reference to a local String. What is the best fix?

Question 5

5.Which is NOT a good way to fix borrow errors?

Read more (free)

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.

From References and borrowing

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

Answer: Arc

Arc uses atomic counting, so it is thread-safe.

From Rc, RefCell and Weak

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

  1. 2 more topics in “Borrowing, slices and lifetimes” before its exam: Slices and strings, Lifetimes.
  2. Module 4 exam: Borrowing, slices and lifetimes · 13 questions · 25 min, without help. 4 topics of this module still to finish first.
  3. Then the next module, Structs, enums and pattern matching. 50 topics left in the course before the final exam.

← Previous: References and borrowing