Skip to content

← Ownership II: Borrowing and the Borrow Checker step 4 of 24

Easy Primitives

How to read a borrow-check diagnostic

The function below is written for you. It does not compile. Your job is to read the error properly and fix it.

pub fn summarize(words: &mut Vec<String>) -> String

What it must do: push one new line onto words of the form "total=N", where N is how many words there were before the push, and return the first word as it was before the push — or "" if the vector was empty.

[]                -> returns "",  words become ["total=0"]
["a"]             -> returns "a", words become ["a", "total=1"]
["a","bb","ccc"]  -> returns "a", words become ["a","bb","ccc","total=3"]

Note the empty case: it must not panic. &words[0] on an empty vector is a panic, so any correct answer has to go through first() or get(0). (Clippy will push you to first() if you reach for get(0) — that is the get_first lint.)

The actual lesson: three spans

Here is what rustc says about the starter:

error[E0502]: cannot borrow `*words` as mutable because it is also borrowed as immutable
 --> src/lib.rs:5:5
  |
3 |     let first = &words[0];
  |                  ----- immutable borrow occurs here
4 |
5 |     words.push(format!("total={}", words.len()));
  |     ^^^^^^^^^^ mutable borrow occurs here
...
7 |     first.to_string()
  |     ----- immutable borrow later used here

Three spans. Beginners fixate on the one with the carets, because that is the one that looks like the error. It is the least useful of the three.

Span 1 — where the loan was created. let first = &words[0];. This is the moment you asked for a view into the vector. Nothing is wrong here yet.

Span 2 — the conflicting action. words.push(...). This is where rustc gave up: pushing needs exclusive access, and the loan from span 1 says somebody else is looking. This is the line with the carets, and it is where people start deleting code. Usually a mistake — the push is the thing you actually wanted to do.

Span 3 — “borrow later used here”. first.to_string(). This is the interesting one, and the one to attack. It is rustc telling you why the loan from span 1 was still alive at span 2. Borrows in Rust end at their last use (item 3.5 is about exactly that). If span 3 did not exist, the loan would have been dead before span 2 and there would be no error at all.

So the reading order is 1, 3, 2: what did I borrow, why is it still alive, and only then, what did I collide with.

The fix follows from span 3

Once you know the problem is “span 3 keeps span 1 alive”, the fixes present themselves in a natural order:

  1. Take a copy of what you need, and drop the loan. You do not want a reference into the vector; you want the first word’s text. Get the text out first, then mutate freely. This is the right answer here.
  2. Move the mutation after the last use of the loan. Sometimes the two just need reordering.
  3. Narrow the loan. Borrow less: one field instead of the whole struct, or one element instead of the whole collection.

And one non-fix worth naming so you can refuse it: cloning the entire Vec to dodge the error. It compiles, it is quadratic, and it tells a reviewer you did not read span 3. In this track .clone() is withdrawn anyway — you have to find the real fix. (Copying a single String‘s contents into a fresh String is a different and much smaller thing; that one is honest here, because the function’s return type genuinely is an owned String.)

A hint about how to get the text out without cloning the Vec

You need an owned String and you have an Option<&String>. One clean route is to start with an empty String and push_str into it inside an if let Some(w) = words.first() { ... } — the loan lives exactly as long as that block and is dead by the time you push.

That is the shape to remember: compute what you need, end the loan, then mutate. It is the most transferable habit in the language, and item 3.6 drills it.

Remember the grade is compile + tests + clippy -D warnings.

Loading visualization…