Reading borrow checker errors as a conversation

The borrow checker has a reputation for being adversarial. I think that is a misreading. Its error messages are unusually generous — they name the lifetime, point at the conflicting borrow, and frequently tell you the fix outright. The difficulty is not that the messages are cryptic. It is that they are long, and long messages get skimmed.

Here is the canonical one. Run it and read what comes back.

fn main() {
    let mut names = vec![String::from("ada")];
    let first = &names[0];
    names.push(String::from("grace"));
    println!("{first}");
}

You will get error[E0502]: cannot borrow `names` as mutable because it is also borrowed as immutable, with three spans: where the immutable borrow starts, where the mutable borrow happens, and where the immutable borrow is later used. That third span is the important one and the one people skip. The borrow is not a problem when it is created — it is a problem because you use it afterwards.

The fix follows from the third span

Delete the println! and the error vanishes. Move the push above the borrow and it vanishes. Clone the string and it vanishes. All three work because all three break the same triangle: borrow, mutate, use.

fn main() {
    let mut names = vec![String::from("ada")];
    names.push(String::from("grace"));
    let first = &names[0];
    println!("{first}");
}

Why the vector case in particular

push may reallocate. If it does, every existing reference into the vector’s buffer becomes a dangling pointer. The borrow checker is not being pedantic about a hypothetical — it is preventing a use-after-free that C++ would hand you without comment. The rule looks restrictive precisely because it is doing work.

The habit worth building: when rustc gives you three spans, read the last one first. It tells you what the compiler thinks you are trying to do, and it is almost always right.