A named value, one bound with let, is dropped at the end of the block
that declares it, and values in the same block are dropped in reverse
declaration order. This article is about the other kind of value: an
unnamed temporary. A temporary is a value an expression produces without
giving it a name, such as:
- the
Stringreturned byfoo()inbar(&foo()); - the
Refguard returned bycell.borrow()on aRefCell; - the
MutexGuardreturned bym.lock().unwrap().
Knowing when a temporary is dropped explains three different failures: the
compile error E0716 ("temporary value dropped while borrowed"), a RefCell
panic, and a lock that is still held when you try to take it again. Two of the
rules changed in edition 2024. This site compiles with edition 2024, but
code and explanations written for edition 2021 or earlier follow the old
rules, so it matters which edition a source is describing.
What a temporary scope is
A temporary is dropped at the end of its temporary scope: the smallest enclosing region of a particular kind. The Rust Reference lists the kinds. The ones that come up most often are:
- a statement, which is the common case: temporaries are dropped at the
;; - the body of an
if,whileorloop, and theelseblock of anif; - the condition of an
iforwhile, and amatcharm's guard; - a
matcharm's body; - each operand of the short-circuiting operators
&&and||; - the whole function body.
The default to remember: a temporary lives until the end of the statement that creates it. That explains this error:
fn bar(s: &String) -> &String { s }
fn foo() -> String { String::from("hi") }
fn main() {
let r = bar(&foo());
// error[E0716]: temporary value dropped while borrowed
println!("{r}");
}
foo() returns a String that has no name. bar returns a reference into
that String, and r holds the reference. The String is dropped at the ;
that ends the let statement, but r is read on the next line, so it would
point at freed memory. The compiler rejects the program. If the
println! line is removed, the program compiles: E0716 is reported only when
the borrow is used after the temporary is dropped.
Temporary lifetime extension
There is one important exception, called temporary lifetime extension. Compare these two lines:
let x = &temp(); // extended: the temporary lives as long as `x`
let y = f(&temp()); // not extended: dropped at the end of this statement
When & is applied directly to a temporary in a let initialiser, the
temporary lives until the end of the block that declares the binding, just
like a named variable. When the borrowed temporary is instead passed to a
function or method call, extension does not apply. The second line is an
error only if f returns something that borrows from its argument and y is
used later.
The full rule in the Reference also extends temporaries through some other
expressions wrapped around the &, including tuple, array, struct and tuple
struct constructors (let w = Wrapper(&temp());), field access
(let s = &temp().field;), and the final expression of a block, if/else
or match arm used as the initialiser. If you are not sure whether extension
applies, give the temporary a name with its own let. Then ordinary scope
rules decide when it is dropped.
Check your understanding
Predict each let. Assume each binding is printed on the line after it. Two of the four compile.
Show answer
Hide answer
fn len(s: &String) -> usize { s.len() }
fn keep(s: &String) -> &String { s }
fn make() -> String { String::from("abcd") }
let a = &make(); // 1
let b = len(&make()); // 2
let c = keep(&make()); // 3
let d = { &make() }.len(); // 4
1 compiles. & is applied directly to a temporary in a let initialiser, so the String is extended to live as long as a.
2 compiles, for a different reason. The temporary is not extended; it is dropped at the end of the statement. But len runs inside that statement and returns a usize, which does not borrow anything. Nothing that refers to the temporary is used after it is dropped.
3 fails with E0716. It has the same shape as 2, except that keep returns a &String that points into the temporary, and c is used after the statement ends. E0716 is not about whether a temporary appears. It is about whether a borrow of the temporary is used after the temporary is dropped. (If c were never used, this line would compile.)
4 fails with E0716 on edition 2024, and compiles on edition 2021. let d = { &make() }; on its own compiles, because extension reaches through a block's final expression. Adding .len() changes that: the block is now the receiver of a method call rather than the initialiser of d, so extension does not apply. Edition 2024 then drops temporaries in a block's final expression at the end of the block, before .len() runs. That is the second edition change described below.
Change 1: if let drops temporaries before else
In an if let, the expression after = is called the scrutinee: the value
being matched against the pattern. In editions up to 2021, temporaries created
by the scrutinee lived for the whole if let expression, including the
else branch. Edition 2024 drops them before the else branch runs.
They still live through the if branch, which may use bindings that borrow
from them.
This example uses a RefCell because a conflicting RefCell borrow panics,
which makes the drop point visible:
use std::cell::RefCell;
fn main() {
let d = RefCell::new(None::<i32>);
if let Some(v) = *d.borrow() {
println!("some {v}");
} else {
*d.borrow_mut() = Some(9);
println!("else branch mutated ok: {:?}", d.borrow());
};
}
d.borrow() returns a temporary Ref guard that marks the cell as borrowed.
The value is None, so the else branch runs and calls borrow_mut(). On
edition 2024 the guard has already been dropped, and the program prints
else branch mutated ok: Some(9). On edition 2021 the guard is still alive,
so borrow_mut() panics with RefCell already borrowed.
The ; after the closing brace of the if let matters on edition 2021.
Without it, the if let is the tail expression of main, and edition 2021
keeps tail-expression temporaries alive until after d itself is dropped.
The guard would then outlive the RefCell it borrows, so edition 2021 rejects
the program with E0597 instead of running it. The ; makes the if let an
ordinary statement, so the program compiles on both editions and shows the
run-time difference. The same applies to the match example below. The
second edition change, described later, is what removes this E0597 on
edition 2024.
The same change applies to locks. With if let Some(v) = *lock.read().unwrap()
on a std::sync::RwLock, the read guard is a scrutinee temporary. On edition
2021, calling lock.write() in the else branch tries to take a write lock
while the same thread still holds a read lock, which the standard library
documents as possibly panicking or deadlocking. On edition 2024 the read guard
has been released before the else branch starts.
The migration lint for this change is if_let_rescope. It is allow-by-default
(it is part of the rust-2024-compatibility lint group used when migrating
code), so ordinary builds do not report it. If code needs the old behaviour,
rewriting the if let as a match keeps it, as the next section shows.
match was not changed
A match scrutinee's temporaries still live until the end of the entire
match, in every edition:
use std::cell::RefCell;
fn main() {
let d = RefCell::new(None::<i32>);
match *d.borrow() {
Some(v) => println!("some {v}"),
None => {
*d.borrow_mut() = Some(9); // panics: RefCell already borrowed
}
};
}
This compiles and then panics at run time on every edition. It has the same shape as the
if let example, but the Ref guard from d.borrow() is still alive inside
the None arm. With a lock in place of the RefCell, the same pattern can
deadlock or panic instead.
So on edition 2024, an if let ... else and the match you might rewrite it
into are not equivalent when the scrutinee creates a guard. The difference
shows up at run time, not as a compile error. Clippy has a lint for this
hazard, significant_drop_in_scrutinee, but it is in the nursery group, so
it is off unless you enable it.
Change 2: temporaries in a block's final expression
A block's tail expression is its last expression, written without a
trailing ;; its value becomes the value of the block. In editions up to
2021, temporaries created in the tail expression were dropped after the
block's local variables. Edition 2024 reverses this: tail-expression
temporaries are dropped before the block's locals.
This function compiles on edition 2024 but not on 2021:
use std::cell::RefCell;
fn f() -> usize {
let c = RefCell::new(String::from("1234"));
c.borrow().len()
}
The tail expression c.borrow().len() creates a temporary Ref guard that
borrows c. On edition 2021 the guard would be dropped after c, so it would
outlive the value it borrows, and the compiler reports E0597 ("borrowed value
does not live long enough"). On edition 2024 the guard is dropped first.
The same change breaks some code that used to compile. This line compiles on edition 2021 but not on 2024:
let x = { &String::from("1234") }.len();
// error[E0716]: temporary value dropped while borrowed
The migration lint is tail_expr_drop_order, which is also allow-by-default
and part of the rust-2024-compatibility group.
Check your understanding
If dropping temporaries earlier fixes code like c.borrow().len(), why does the change need an edition?
Show answer
Hide answer
Because some existing code depends on the old, later drop point, and changing it in every edition would break that code.
The first kind of dependence is borrow checking. Under edition 2021, the temporary String in { &String::from("1234") } lived until the end of the enclosing statement, so the reference the block returned was still valid when .len() ran. Under edition 2024, that temporary is dropped at the end of the block, so the reference would be used after the value is freed, and the compiler rejects the program.
The second kind does not involve the borrow checker at all: the order in which Drop implementations run can be observable. If two values' destructors interact, such as a guard and the resource it protects, or a transaction and the connection it uses, running them in a different order can change what the program does at run time without changing any types.
Because drop order is part of a Rust program's meaning, the language changes it only at an edition boundary. Each crate chooses its edition, so existing code keeps the old behaviour until it is migrated, and the migration lints point out the places that may change.
The practical rules
- A temporary is dropped at the end of its enclosing statement, unless temporary lifetime extension applies.
let x = &temp();extends the temporary.let x = f(&temp());does not.- When E0716 is confusing, give the temporary a name with its own
let. That turns it into an ordinary local whose drop point is easy to see. - On edition 2024,
if letdrops scrutinee temporaries before theelsebranch, whilematchkeeps them until the wholematchends. - Avoid creating a lock guard or
RefCellguard in a scrutinee when any arm or branch needs to lock or borrow the same value again. Bind the guard to a local, or copy the value out in an earlier statement, so you choose where the guard is dropped.
When reading older material about temporaries, check which edition it
assumes. Edition 2024 became stable in Rust 1.85 (February 2025), and the
if let and tail-expression rules above differ from earlier editions.