9.3The borrow checker is your friend
No new rules today. You have both of them already: one writer or many readers, and references must always be valid. This lesson is a clinic. Four programs walk in with the four complaints every new Rust programmer hears in their first week, and for each one we'll read the compiler's diagnosis line by line, identify the actual disease, and apply the fix. The fixes are worth collecting, because there are only four of those too: add mut, end the borrow earlier, borrow instead of move, or reorder.
One reframe before the first patient. New Rust programmers tend to experience this chapter's errors as the compiler being difficult. The C++ versions of all four programs below compile without a word, and three of them then read freed memory or worse. The borrow checker is the colleague who points at the bug during code review instead of letting you ship it. It's abrupt, it's always right, and it works for free.
Patient 1: forgot the mut (E0596)
fn main() {
let s = String::from("hello");
let r = &mut s;
r.push_str(", world");
println!("{r}");
}error[E0596]: cannot borrow `s` as mutable, as it is not declared as mutable
--> src/main.rs:3:13
|
3 | let r = &mut s;
| ^^^^^^ cannot borrow as mutable
|
help: consider changing this to be mutable
|
2 | let mut s = String::from("hello");
| +++
Read the headline carefully: "as it is not declared as mutable." This isn't a borrow-rules violation at all; it's lesson 1.4 wearing chapter 9 clothing. You can't take a writable loan against a variable the owner declared unchangeable, for the same reason you can't get a key to a room the owner sealed. The help block even writes the fix as a diff: add mut on line 2. Every &mut needs a mut binding behind it, all the way up the chain of consent from last lesson.
Patient 2: writing while a view is watching (E0502)
Here's lesson 5.4's machinery failing honestly for the first time. trim hands back a view into the String's own text, and views are borrows:
fn main() {
let mut name = String::from(" Ada ");
let trimmed = name.trim();
name.push_str("!");
println!("Hello, {trimmed}!");
}error[E0502]: cannot borrow `name` as mutable because it is also borrowed as immutable
--> src/main.rs:4:5
|
3 | let trimmed = name.trim();
| ---- immutable borrow occurs here
4 | name.push_str("!");
| ^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
5 | println!("Hello, {trimmed}!");
| ------- immutable borrow later used here
The three-act structure from last lesson, with a twist: nobody wrote a single & in this program. Methods borrow on your behalf. Calling name.trim() takes a read-only borrow of name (and trimmed keeps it alive), while name.push_str(...) takes a writable one. The borrow checker sees through the method syntax to the loans underneath.
And this refusal is the one from last lesson's horror story. If push_str had run, it could have forced the String to relocate its text to a bigger heap block, leaving trimmed pointing into freed memory, which line 5 would then have printed. In C++, the equivalent program compiles, runs, and prints garbage on Tuesdays. The fix is the reordering trick: trimmed's last use is the println!, so move that up.
fn main() {
let mut name = String::from(" Ada ");
let trimmed = name.trim();
println!("Hello, {trimmed}!");
name.push_str("!");
}
Reader finishes, then the writer goes. Compiles, and prints Hello, Ada!.
Patient 3: moved when it meant to lend (E0382)
You met E0382 in lesson 8.4 as the use-after-move error. In week-one practice it almost always arrives in this specific costume: a helper function that takes ownership it never needed.
fn shout(text: String) -> String {
text.to_uppercase()
}
fn main() {
let cheer = String::from("goal");
let loud = shout(cheer);
println!("{cheer} became {loud}");
}error[E0382]: borrow of moved value: `cheer`
--> src/main.rs:8:16
|
6 | let cheer = String::from("goal");
| ----- move occurs because `cheer` has type `String`, which does not implement the `Copy` trait
7 | let loud = shout(cheer);
| ----- value moved here
8 | println!("{cheer} became {loud}");
| ^^^^^ value borrowed here after move
|
note: consider changing this parameter type in function `shout` to borrow instead if owning the value isn't necessary
--> src/main.rs:1:16
|
1 | fn shout(text: String) -> String {
| ----- ^^^^^^ this parameter takes ownership of the value
| |
| in this function
help: consider cloning the value if the performance cost is acceptable
|
7 | let loud = shout(cheer.clone());
| ++++++++
This transcript offers two ways out, and choosing between them is the lesson. The help block suggests cloning, lesson 8.7's tool, and 8.7 warned you about exactly this moment: cloning here works, but it duplicates the text just to throw the copy away. The note block has the real diagnosis, and notice it points at the function, not the call: "consider changing this parameter type ... to borrow instead if owning the value isn't necessary." It isn't. shout only reads text, so it should ask for a view:
fn shout(text: &str) -> String {
text.to_uppercase()
}
fn main() {
let cheer = String::from("goal");
let loud = shout(&cheer);
println!("{cheer} became {loud}");
}goal became GOAL
The parameter follows lesson 5.4's best practice (&str, not &String; the last piece of why falls in lesson 9.7), the call site lends instead of gives, and cheer is alive for line 8. The clones you were told to let wait a chapter? This is what they were waiting for.
Patient 4: replacing a value someone's watching (E0506)
fn main() {
let mut label = String::from("Score: 10");
let view = &label;
label = String::from("Score: 20");
println!("{view}");
}error[E0506]: cannot assign to `label` because it is borrowed
--> src/main.rs:4:5
|
3 | let view = &label;
| ------ `label` is borrowed here
4 | label = String::from("Score: 20");
| ^^^^^ `label` is assigned to here but it was already borrowed
5 | println!("{view}");
| ---- borrow later used here
Why should assignment bother a borrow? Lesson 8.4's fine print: assigning into an existing variable drops the old value first. Line 4 would bulldoze the very String that view is watching, and line 5 would read the rubble. Same disease as patient 2, different surgery site: there the old text was endangered by growth, here by demolition. Same fix, too. Print first, assign after, and the borrow checker waves it through.
Tip
When a borrow error has you stuck, find the last use of the complaining reference and ask: can this line move up, before the write? Can the write move down? Nine times out of ten the program was correct in spirit and just had its lines in an order the loans can't take turns in.
Quiz time
Question #1
From memory (no compiler): match each error code (E0382, E0502, E0506, E0596) to its one-line description.
a) cannot borrow as mutable because it is also borrowed as immutable b) borrow of moved value c) cannot borrow as mutable, as it is not declared as mutable d) cannot assign because it is borrowed
Show solution
a) E0502. b) E0382. c) E0596. d) E0506. Worth the memorizing: you'll be greeting these four by name for your whole Rust career, and recognizing the code before reading the message is a real speed boost.
Question #2
Diagnose (name the error code) and fix this program without removing or changing the meaning of any line. It should print the original and the trimmed length.
fn main() {
let mut input = String::from(" 42 \n");
let cleaned = input.trim();
input.push_str("(processed)");
println!("cleaned: {cleaned}");
println!("raw: {input}");
}Show solution
E0502: cleaned is a read-only view into input (patient 2's disease), still alive at line 5, while push_str wants a writable loan at line 4. Reorder so the reader finishes first:
fn main() {
let mut input = String::from(" 42 \n");
let cleaned = input.trim();
println!("cleaned: {cleaned}");
input.push_str("(processed)");
println!("raw: {input}");
}cleaned: 42
raw: 42
(processed)
(The raw line really does print the leftover whitespace and newline before (processed); that's what "raw" means.)
Question #3
Diagnose and fix, choosing the cheapest correct fix:
fn banner(title: String) -> String {
title.to_uppercase()
}
fn main() {
let name = String::from("results");
let top = banner(name);
println!("== {top} ==");
println!("(section: {name})");
}Show solution
E0382, patient 3's costume: banner takes ownership it doesn't need, so name is dead by the last line. The cheap fix is borrowing, not cloning: change the signature to fn banner(title: &str) -> String and the call to banner(&name). Now nothing is copied, nothing moves, and both println! lines work. banner(name.clone()) also compiles, but it pays for a full copy of the text to fix what one & fixes for free.
Four diseases, four fixes, zero new rules. What's still missing is judgment: when should a function take ownership, a view, or a writable loan? That's not an error message question, it's a design question, and it gets the next lesson to itself.