Reading Rust

Part 2 Β· The compile-time machine I β€” borrows & lifetimes

The borrow checker's errors are the curriculum

Nine error codes cover almost everything the borrow checker will ever say to you. Each one reveals a specific piece of the model.

45 minbeacon traininginterleavingPOE
What this lesson is trying to break (3)
  • Borrow errors are a wall of noise to be pattern-matched away.
  • Every borrow error has a different cause.
  • Adding `.clone()` is the general fix.

You have the model. This lesson makes it operational.

Almost every borrow-checker error you will meet is one of nine codes. Each one corresponds to a specific piece of the model, so learning them is not memorising error strings β€” it is a diagnostic index into Part 2.

Run rustc --explain E0502 in your terminal at some point today. The compiler ships an explanation for every code, with examples, and it is the best-hidden teaching resource the Rust project has.

The table

Varies: which pair of things collided

CodeSaysWhat actually happenedUsual fix
E0382use of moved valueYou used a binding after it was moved away.Borrow instead of moving; or clone; or restructure so the move is last.
E0499cannot borrow as mutable more than onceTwo &mut to the same data overlap.Shorten one region; split the borrow into disjoint fields; use indices instead of references.
E0502cannot borrow as mutable, also borrowed as immutableA & and a &mut overlap.Finish using the shared borrow before mutating; compute values first, then mutate.
E0505cannot move out, it is borrowedYou moved a value while a reference to it was still live.Let the borrow end first; or clone.
E0506cannot assign to borrowedYou overwrote a value while a reference to it was live.Same as E0505 β€” end the borrow first.
E0507cannot move out of shared referenceYou tried to take ownership of something you only borrowed..clone(); borrow instead; or Option::take / mem::replace / HashMap::remove.
E0515cannot return reference to localThe reference points into a frame that is about to vanish.Return owned data. Never annotations.
E0597does not live long enoughA value’s scope ends before a reference to it is used.Move the declaration outward, shrink the use, or own the data.
E0716temporary value dropped while borrowedYou borrowed from a temporary that died at the semicolon.let-bind the temporary first.

The top six are the shared-XOR-exclusive rule from 2.1, in six different disguises. The bottom three are region problems from 2.3.

Notice the shape of that table. Six of the nine are the same rule. They differ only in which two references collided and which one the compiler reported first. If you can see that, borrow errors stop feeling like nine unrelated problems.

The three that need more than a table

E0507 β€” cannot move out of a shared reference

The most common error for people arriving from garbage-collected languages, because in those languages there is no difference between β€œlook at” and β€œtake”.

struct Config { name: String }

fn take_name(c: &Config) -> String {
    c.name              // error[E0507]
}

You have a &Config β€” permission to look. Returning c.name would move the String out, leaving a hole in a struct you do not own. The compiler is protecting the owner, who has no idea this is happening.

Three fixes, and the choice is a real decision:

fn take_name(c: &Config) -> String { c.name.clone() }   // allocate
fn take_name(c: &Config) -> &str   { &c.name }          // borrow β€” no allocation
fn take_name(c: Config)  -> String { c.name }           // demand ownership

The middle one is usually right and usually not the one people reach for first.

Beaconsee this β†’ think this, without decoding it

std::mem::take Β· std::mem::replace Β· Option::take

Someone needed to move a value out of a place they only have &mut to.

You cannot leave a hole β€” but you can swap in a replacement. mem::take puts Default::default() there, mem::replace puts a value you supply, and Option::take leaves None.

Seeing these is a reliable sign that the author is restructuring ownership rather than reaching for .clone(), and it is a good pattern to be able to name in review.

E0515 β€” cannot return reference to local variable

fn make() -> &String {
    let s = String::from("hi");
    &s                       // error[E0515]
}

Every newcomer tries to fix this with lifetime annotations. Nothing in the lifetime system can fix it, and understanding why is a good test of lesson 2.3.

The reference points into make’s stack frame. That frame is destroyed on return. There is no region containing β€œafter this function returns” over which s is valid, so no annotation can describe one. The problem is not the description; it is the program.

Return String. Or take a &mut String parameter so the buffer belongs to the caller.

E0716 β€” temporary value dropped while borrowed

Straight out of 1.4.

let r = &String::from("hi").len();   // hmm
let name = &config().name;           // error[E0716]

config() returns a value with no name. It is a temporary; it dies at the semicolon; name would dangle.

let cfg = config();          // now it has a place
let name = &cfg.name;        // fine β€” borrows from a local

The fix is always the same: give the temporary a name. When you see E0716, look for the expression that produced a value nobody bound.

Beacon drill

Read each snippet and name the error code before opening the answer. Speed matters more than certainty here β€” you are training recognition, not derivation.

Predict firstcommit in writing, then look
let mut v = vec![1, 2, 3];
let a = &mut v;
let b = &mut v;
a.push(4);
Predict firstcommit in writing, then look
let s = String::from("hi");
let r = &s;
drop(s);
println!("{r}");
Predict firstcommit in writing, then look
fn first(v: &Vec<String>) -> String {
    v[0]
}
Predict firstcommit in writing, then look
let mut scores = std::collections::HashMap::new();
scores.insert("a", 1);
let first = scores.get("a").unwrap();
scores.insert("b", 2);
println!("{first}");
Predict firstcommit in writing, then look
fn longest_line(text: &str) -> &str {
    let owned = text.to_uppercase();
    owned.lines().max_by_key(|l| l.len()).unwrap()
}
Build your own procedurewrite it β€” thinking it does not count

Write down the diagnostic procedure you will actually use when you hit a borrow error. Three or four steps, in order, in your own words.

Mine, for comparison β€” but write yours first: (1) which two things overlap? (2) which one can I make end sooner? (3) if neither, can I make the disjointness visible β€” separate fields, indices, split methods? (4) only then, is cloning or owning the honest answer?

What you should now be able to say

  • Nine codes cover the field, and six are one rule in different disguises.
  • E0507 is the look versus take distinction, and &str is often the fix people miss.
  • E0515 is unfixable by annotation β€” the frame is gone.
  • E0716 means an unnamed temporary; bind it.
  • rustc --explain E0XXX exists and is good.
  • Fix borrow errors by shortening regions and making disjointness visible, before reaching for .clone().

Next: 2.6 β€” The escape hatches. What do you do when the rule is genuinely too strict for the data structure you need?

In the wildgo read the real thing now, while it is fresh
  • The rustc error indexEvery error code with a minimal example and an explanation. `rustc --explain E0502` prints the same thing in your terminal. This is the best-hidden teaching resource in the Rust project.
  • rustc error code E0502 in the compiler sourceThe explanations are just markdown files in the compiler repo. Worth seeing once β€” it demystifies the diagnostics and shows how much deliberate teaching effort goes into them.
Scheduled for recall4 items added to your review queue

These are not shown to you now on purpose. They will surface in Review tomorrow, mixed in among items from other parts of the course β€” not grouped by topic. Sorting out which idea applies is the part that transfers.