9.2Mutable references

Last updated June 12, 2026

In lesson 1.6 you wrote this line on faith, and a callout box promised that "lesson 9.2 pays this off exactly":

io::stdin().read_line(&mut name).expect("failed to read line");

This is that lesson. After eight chapters, every word of the input recipe is finally yours: read_line appends what the user typed to name (lesson 1.6), .expect is the blunt error tool (lesson 3.x's crash-site message, properly retired in chapter 12), and &mut name... lends read_line a reference through which it is allowed to write. The function needs to put the user's text somewhere that's yours, so a read-only window won't do. It needs a mutable reference.

&mut on both sides

Last lesson's add_world was refused for writing through a plain &. Here it is with permission granted:

fn add_world(text: &mut String) {
    text.push_str(", world");
}

fn main() {
    let mut message = String::from("hello");
    add_world(&mut message);
    println!("{message}");
}
hello, world

Count the places mutation is written down, because there are three and all of them are mandatory. The binding is let mut message (without it, nothing may change the variable at all, lesson 1.4's rule). The call site says &mut message: the owner consents, in writing, to this particular loan being a writable one. And the signature says text: &mut String: the function declares what it intends to do. Owner consents, function declares, compiler checks that all three stories match.

Author's note

learncpp's chapter on this (12.13) spends paragraphs on a genuine C++ pain: at a C++ call site, getSinCos(degrees, sin, cos) gives no hint that sin and cos are about to be overwritten. You have to go read the function's declaration to find out. I get to teach the language where that complaint is impossible: in Rust, a call that can modify your variable says &mut right there at the call site, or it doesn't compile. The annoying extra typing is the documentation.

So read_line(&mut name) now parses completely: "here is a writable loan of name; fill it." That's also why lesson 1.7's most famous beginner error happens. Forget the &mut and you've offered to give read_line the String (a move, chapter 8) when it asked for a writable loan; the types disagree and the compiler hands you the fix.

One writer at a time

Plain references could be handed out freely, any number at once. Mutable references come with the restriction that gives rule 1 its teeth: while a mutable reference to a value exists, no other reference to that value may exist at all. One writer, or many readers, never both, and never two writers. Try to take two writable loans and the borrow checker steps in:

fn main() {
    let mut s = String::from("hello");

    let r1 = &mut s;
    let r2 = &mut s;

    println!("{r1}, {r2}");
}
error[E0499]: cannot borrow `s` as mutable more than once at a time
 --> src/main.rs:5:14
  |
4 |     let r1 = &mut s;
  |              ------ first mutable borrow occurs here
5 |     let r2 = &mut s;
  |              ^^^^^^ second mutable borrow occurs here
6 |
7 |     println!("{r1}, {r2}");
  |                -- first borrow later used here

This error's shape is worth memorizing, because the borrow checker always tells its stories in three labeled acts: where the first borrow happened, where the conflicting borrow happened, and where the first borrow is still being used later. All three lines matter. If that third line didn't exist, neither would the error, which is the escape hatch we'll get to in a moment.

Mixing flavors is refused just as firmly. Readers are promised the value won't change under them, so a writer can't move in while they're watching:

fn main() {
    let mut s = String::from("hello");

    let r1 = &s; // no problem
    let r2 = &s; // no problem
    let r3 = &mut s; // problem!

    println!("{r1}, {r2}, and {r3}");
}
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
 --> src/main.rs:6:14
  |
4 |     let r1 = &s; // no problem
  |              -- immutable borrow occurs here
5 |     let r2 = &s; // no problem
6 |     let r3 = &mut s; // problem!
  |              ^^^^^^ mutable borrow occurs here
7 |
8 |     println!("{r1}, {r2}, and {r3}");
  |                -- immutable borrow later used here

Why so strict?

Because the alternative is the bug class that has cost the software industry the most sleep. If one part of a program can change a value while another part is reading it, the reader sees half-finished states. Run those two parts on different processor cores at the same time (threads, chapter 22) and you have a data race, a bug that corrupts data based on which core happens to win a footrace, unreproducible by design. Rust's headline feature is that its compiler makes data races impossible, and this rule is how: code that could race doesn't compile.

But you don't need threads to get burned, and chapter 5's bicycle is the cleanest way to see it. A &str view into a String is a window onto the owner's heap text (lesson 5.4). Now let a writer push more text in while that view exists: growing a String can force it to relocate to a bigger heap block (the move-to-a-bigger-block story lesson 8.2 told about push_str), and the view is left pointing at the old, freed address. The neighbor stares at an empty driveway, except this time the neighbor is your program, reading memory that's been handed back. C++ allows exactly this, and lesson 9.7 will show you Rust refusing it with a transcript. One-writer-or-many-readers isn't strictness for its own sake; it's the precise condition under which a view can never lie to you.

Borrows end at their last use

The good news, and the fact that dissolves most fights with the borrow checker: a borrow doesn't last until the closing brace. It lasts until the last place it's used. Watch both rules relax in a perfectly legal program:

fn main() {
    let mut s = String::from("hello");

    let r1 = &s;
    let r2 = &s;
    println!("{r1} and {r2}");

    let r3 = &mut s;
    r3.push_str(", world");
    println!("{r3}");
}
hello and hello
hello, world

r1 and r2 take their last bow in the first println!, so by the time r3 is created the readers are gone and the writer is welcome. Sequential loans are always fine; it's overlapping ones the rules constrain. When the borrow checker refuses your program, the first thing to check is whether reordering a line or two makes the borrows take turns instead of overlap. It usually does.

Key insight

A reference's life runs from where it's created to where it's last used, not to the end of the block. The compiler tracks this per reference, which is why "move the println! up a line" is a real and honorable fix for a borrow error.

Quiz time

Question #1

For each program, say whether it compiles. If it does, give the output; if not, name the rule it breaks.

a)

fn main() {
    let mut word = String::from("hi");
    let r = &word;
    word.push_str("!");
    println!("{r}");
}

b)

fn main() {
    let mut word = String::from("hi");
    let r = &word;
    println!("{r}");
    word.push_str("!");
    println!("{word}");
}

c)

fn main() {
    let mut word = String::from("hi");
    word.push_str("!");
    let r = &word;
    println!("{r}");
}
Show solution

a) Refused, E0502. push_str needs a writable borrow of word (it's a method that modifies, so it borrows &mut behind the scenes), but the reader r is still alive: it gets used in the println! afterward. One writer or many readers, never both at once.

b) Compiles, printing hi then hi!. Same four lines as (a), different order: r's last use is the first println!, so the borrow has ended before push_str needs the value. Taking turns is always legal.

c) Compiles, printing hi!. The write happens first, the reader is created after. No overlap, no complaint.

Question #2

Why does read_line need &mut name rather than &name? Answer in ownership-and-borrowing words.

Show solution

read_line's whole job is to modify the String you lend it: it appends the user's typed line to it. A &name loan is read-only, and the compiler holds functions to that promise, so read_line couldn't do its job through one. It asks for &mut String: a writable loan. It borrows rather than takes ownership because the buffer is yours: you'll want to trim and use it after the call, and lesson 8.6's give-it-back dance would be a terrible API for something called ten times per program.

Question #3

Without changing any line's text, reorder this program's main so it compiles, and give its output:

fn shout(text: &mut String) {
    text.push_str("!!!");
}

fn main() {
    let mut cheer = String::from("goal");
    let watcher = &cheer;
    shout(&mut cheer);
    println!("{watcher}");
    println!("{cheer}");
}
Show solution

Move the watcher's lines before the shout (reader takes its turn, then the writer):

fn main() {
    let mut cheer = String::from("goal");
    let watcher = &cheer;
    println!("{watcher}");
    shout(&mut cheer);
    println!("{cheer}");
}
goal
goal!!!

The original overlapped a live reader with a writable loan (E0502: watcher was created before the shout call and used after it). Reordered, watcher's last use comes first, its borrow ends, and shout gets the value to itself.

You now know both kinds of loans and the rule that governs them. Next lesson is pure practice: the four borrow-checker errors every Rust programmer meets in week one, each read line by line and fixed, so that when they happen to you they'll feel like old acquaintances.