Skip to content
Select theme

How to end a borrow early

Short answer: A borrow ends at the last use of the reference, not at the end of the block. So a borrow that “won’t die” is really a reference — or a value holding one, like the &i32 inside an Option<&i32> — that is still used further down. End it early by closing an inner scope around the borrow, by dropping the reference once the work is done, or by taking the value instead of the reference.

Start from a fresh project — cargo new end-borrow-early, then replace src/main.rs with the whole of each file below.

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

Read the spans in the error. The borrow of scores did not end on line 3, where it was created, and it did not end when line 3 finished — it reaches all the way to line 5, because first is used there. first is an Option<&i32>: it holds a reference to inside the vector, and while that reference is still alive, so is the borrow. The push on line 4 needs &mut scores, and it lands in the middle of a live borrow. The reference outlived the line that made it, and it dragged the borrow along.

(How long does a borrow last? covers the last-use rule in general. This page is about what to do when the rule works against you.)

Braces end a borrow at a point you choose. Everything that needs the reference goes inside a block; at the closing brace the reference dies, the borrow ends, and scores is free again:

first was Some(10)
[10, 40, 30, 0]

The block costs nothing at run time. It only marks the region where the borrow should live, the same way the borrow of a let ends when the variable’s own block ends. This is the pattern to reach for when the borrow genuinely covers several statements and they belong together — read through the collection inside the block, mutate it after.

The other tool is drop, applied to the reference itself:

[10, 40, 35, 0]

It looks odd — dropping a reference? — but it compiles and it works. drop(top) moves the reference into drop, which makes that call the reference’s last use, so the mutable borrow of scores ends on line 5 and the push on line 6 is legal.

rustc does have something to say about it:

warning: calls to `std::mem::drop` with a reference instead of an owned value does nothing
--> src/main.rs:5:5
|
5 | drop(top);
| ^^^^^---^
| |
| argument has type `&mut i32`
|
= note: `#[warn(dropping_references)]` on by default
help: use `let _ = ...` to ignore the expression or result
|
5 - drop(top);
5 + let _ = top;
|
warning: 1 warning emitted

The warning is right about run time and silent about the borrow. At run time, dropping a reference does nothing: a reference is a pointer, dropping it frees nothing, and the vector is untouched. The borrow still ends, because for the borrow checker the drop counts as the reference’s final use.

That also states the limit. drop cannot rescue a reference the code still needs: if top were used after the push, the borrow would reach past the drop, because a later use is a longer borrow. And since the borrow would have ended at top’s last use anyway, drop on a plain reference mostly makes the ending explicit instead of implicit. Where it does real work is on values that hold a borrow with a run-time side to it — the guard returned by RefCell::borrow_mut, for instance, keeps the cell borrowed until the guard is dropped, and drop(guard) releases it in the middle of a block.

Often the cleanest fix is to stop holding the reference. If all you need from first is the number, take the number:

Some(10)
[10, 40, 30, 0]

.copied() turns Option<&i32> into Option<i32>: the value comes out of the borrow, the reference dies on the spot, and the borrow of scores ends on the line it was made. The same trick works for a mutable borrow — let bumped = *top; copies the i32 out, and that copy is top’s last use.

This is the shape to look for when a borrow “won’t die”: a reference being kept around for later when the later code only wants the value. For Copy types like i32, copying the value out is free and ends the borrow immediately. For types that are not Copy, the scope or the reordering is the fix — and if you find yourself wanting to clone a String out of a borrow, that is the signature question from the previous article, not a borrow-lifetime one.