Skip to content
Select theme

Why does Rust allow many immutable borrows but only one mutable?

Short answer: The combination Rust forbids is not reading plus writing, it’s aliasing plus mutation. Shared borrows can’t change anything, so no reader can invalidate another and any number of them can be alive at once. A mutable borrow can change everything, including where the value’s memory lives, so it has to be exclusive: while it exists, nothing else may be looking at the value.

A & hands out read access and nothing else. Two shared borrows see the same bytes from the first line to the last, and neither can be the reason the other goes wrong. That’s why the rule puts no number on them: any count of & borrows is fine. The compiler only asks whether a &mut overlaps one.

A writer can move the memory under a reader’s feet

Section titled “A writer can move the memory under a reader’s feet”

Borrow one element of a Vec, then push to it. From a fresh project — cargo new borrow-why, then replace src/main.rs with the whole of this. The project name doesn’t matter, because nothing in the output depends on it:

error[E0502]: cannot borrow `scores` as mutable because it is also borrowed as immutable
--> src/main.rs:4:5
|
3 | let first = &scores[0];
| ------ immutable borrow occurs here
4 | scores.push(4);
| ^^^^^^^^^^^^^^ mutable borrow occurs here
5 |
6 | println!("{first}");
| ----- immutable borrow later used here
For more information about this error, try `rustc --explain E0502`.

The push is refused, and the reason is not tidiness. push can grow the buffer: allocate a bigger block, copy the elements across, free the old one. A reference taken before the push would then point at freed memory. Here the Vec starts at exactly its capacity of three, so the fourth element forces that move:

3
6

Without the rule, the first program compiles and first is left pointing at the freed buffer: the classic dangling-reference bug, in the same family as the invalidated C++ iterator. Rust turns it into a compile error instead.

The everyday shape: iterating while pushing

Section titled “The everyday shape: iterating while pushing”

The same collision, in its usual form:

error[E0502]: cannot borrow `numbers` as mutable because it is also borrowed as immutable
--> src/main.rs:5:9
|
4 | for n in &numbers {
| - --------
| | |
| | immutable borrow occurs here
| _____| immutable borrow later used here
| |
5 | | numbers.push(n * 2);
| | ^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
6 | | }
| |_____- this for loop borrows `numbers` immutably, preventing mutation within its body
|
= help: consider using an index-based loop instead, or collecting modifications into a separate collection
For more information about this error, try `rustc --explain E0502`.

The loop holds a shared borrow of numbers for its whole body, so the push inside it overlaps a reader. The iterator points into the buffer; a push in the middle of the loop can move that buffer, and the iterator would keep reading the old one. The compiler’s own suggestion is the shape of the fix: an index-based loop, or collecting the changes first. When the change doesn’t depend on reading other elements, iter_mut() is shorter still.

A function that takes &mut Vec<i32> can resize the buffer without asking:

[1, 2, 3, 4]

Exclusivity is a promise from the caller: nothing else is reading the value while the function runs, so growing the buffer is safe. That is the promise the first example couldn’t make, and it travels in the signature. At the call site the binding needs mut; why the keyword is required has that error.

When you want to change a value while looping over it

Section titled “When you want to change a value while looping over it”

Take the mutable references one element at a time instead of borrowing the whole collection:

[2, 4, 6]

iter_mut() hands out one &mut i32 per iteration, so the loop itself is the single exclusive borrow, and *n *= 2 writes through it. When a change depends on reading several elements at once, collect the decisions first and apply them after the loop. When even that doesn’t fit, Cell and RefCell move the borrow check to run time; that trade gets its own chapter, later.