Rust drop order: locals, fields, temporaries, and why let _ drops right away
Rust drops locals in reverse order and struct fields in declaration order. Temporaries usually die at the end of the statement, and let _ binds nothing. Plus Rust 2024's scope changes.
In Rust, values that clean up when a scope ends, such as mutex guards, file handles and transactions, behave according to when Drop runs. Most of the time that matches intuition. Then a line like let _ = mutex.lock().unwrap(); turns out to release the lock on the very line that was supposed to take it, or an if let deadlocks because its else block tries to take the same lock again.
This post works through the drop order rules using the destructors chapter of The Rust Reference and the Rust 2024 edition guide. The level is intermediate: it assumes you know ownership and the Drop trait.
The basic rules
Summarized from the Reference:
- Every variable and temporary belongs to a drop scope. When control flow leaves that scope, its variables are dropped in reverse order of declaration and its temporaries in reverse order of creation.
- A local declared with
letbelongs to the scope of the block containing thelet. - Function parameters belong to the scope of the whole function body, so they are dropped last when the function finishes.
- Dropping one value first calls
Drop::dropif the type implements it, then recursively drops its fields. Struct fields drop in declaration order, tuple elements in order, and elements of an array or owned slice from the first to the last. - Variables bound inside one pattern drop in reverse order of their declaration within the pattern.
Locals go in reverse, fields go forward. Once you remember that the two run in opposite directions, most of what follows comes from it.
let _ binds nothing
The patterns chapter of the Reference says of the wildcard pattern _: unlike identifier patterns, it does not copy, move, or borrow the value it matches. A name that starts with an underscore, like _c, is an ordinary identifier pattern that creates a real binding: it copies a Copy value and moves a non-Copy value. The Noisy value in the example below is moved, because Noisy is not Copy. They look alike but behave differently.
The temporary rules add to this. The initializer of a let statement is a place expression context, and when a value expression such as a function call appears there, an unnamed temporary is created. That temporary’s drop scope is usually the end of the statement. Binding by reference, as in let x = &temp();, extends the temporary to the end of the block, but _ binds nothing, so there is no extension.
Here is an example.
struct Noisy(&'static str);
impl Drop for Noisy {
fn drop(&mut self) {
println!("drop {}", self.0);
}
}
struct Pair {
first: Noisy,
second: Noisy,
}
fn main() {
let a = Noisy("a");
let _ = Noisy("b");
let _c = Noisy("c");
let pair = Pair { first: Noisy("pair.first"), second: Noisy("pair.second") };
let _ = a;
println!("end of main");
}
The output below is derived from the Reference rules. It was not produced by running the program in the environment this post was written in.
drop b
end of main
drop pair.first
drop pair.second
drop c
drop a
- In
let _ = Noisy("b");,Noisy("b")is a temporary that_does not bind, so it is dropped as soon as thatletstatement ends. That is why it prints beforeend of main. let _ = a;does not movea. Nothing happens on that line;astays where it is and is dropped whenmainends.- At the end of
main, variables drop in reverse declaration order:pair,_c,a. - Dropping
pairdrops its fields in declaration order:first, thensecond.
Where this bites: locks
The rustc lint documentation gives this example:
let _ = data.lock().unwrap();
println!("doing some work");
The MutexGuard returned by data.lock().unwrap() is a temporary that _ does not bind, so it is dropped as soon as the statement ends. The lock is released immediately and the next line runs without it. rustc catches this with the let_underscore_lock lint, which was deny by default in the lint listing checked on 2026-10-14, so it is a compile error.
That lint only covers synchronization locks. For other values that implement Drop, such as a guard type you wrote yourself, the relevant lint is let_underscore_drop, which is allow by default, so the code passes silently unless you turn it on. The lint documentation suggests two fixes:
- To keep the value until the end of the scope, bind it to a name that starts with an underscore, such as
let _guard = .... - If dropping it immediately is really what you want, call
drop(...)to make that intent visible.
std::mem::drop is not special. It is an ordinary function defined as pub fn drop<T>(_x: T) {}: it takes the value by move, and the value is dropped when the function returns. For a Copy type, a copy is passed in, so the original is unaffected.
Temporary scopes
Without lifetime extension, a temporary’s drop scope is the smallest of the following that contains the expression. These are the cases from the Reference’s list you meet most often:
- A statement
- The body of an
if,whileorloop, and theelseblock of anif - The ordinary (non-pattern-matching) condition of an
iforwhile - Each operand of
&&and|| - The body of a
matcharm - The entire tail expression of a block (2024 edition)
- The condition and then block of an
if let(2024 edition)
The scrutinee of a match is not on this list. The Reference says so explicitly, and as a result temporaries created in the scrutinee live past the match expression, usually to the end of the statement. If you write match *value.read().unwrap() { ... }, the read lock is held while any arm runs. This is the same in every edition.
Two changes in the Rust 2024 edition
According to the edition guide, the Rust 2024 edition was stabilized in Rust 1.85.0, and it adds two rules that narrow temporary scopes. The edition is a per-crate setting; with Cargo it is the edition key under [package] in Cargo.toml. The Cargo documentation says that if the key is absent, the 2015 edition is assumed.
if let temporaries drop before else
This is the edition guide’s example:
use std::sync::RwLock;
fn f(value: &RwLock<Option<bool>>) {
if let Some(x) = *value.read().unwrap() {
println!("value is {x}");
} else {
let mut v = value.write().unwrap();
if v.is_none() {
*v = Some(true);
}
}
}
Up to the 2021 edition, the read lock temporary from value.read() lived until the whole if let expression ended, meaning after the else block. Entering else and trying to take the write lock therefore deadlocks. In the 2024 edition the temporary is dropped once the then block finishes or control enters else, so this code works as intended.
Some code may instead rely on the old behaviour of keeping the temporary alive past else. The edition guide notes that rewriting if let as a match is always safe: as shown above, temporaries in a match scrutinee live past the match, which matches 2021 if let. cargo fix --edition proposes such match rewrites through the if_let_rescope lint, and the guide recommends reviewing each change afterwards to see whether you need it. In the example above, converting to match brings the deadlock back.
Tail expression temporaries drop before locals
Up to the 2021 edition, temporaries created in the last expression of a block or function body (the tail expression) were dropped after the block’s local variables. That is why this code fails in 2021 with E0597, saying c does not live long enough:
use std::cell::RefCell;
fn f() -> usize {
let c = RefCell::new("..");
c.borrow().len()
}
In the 2024 edition the temporary from c.borrow() is dropped first and c after it, so it compiles.
The same change makes some code stop compiling in 2024. The edition guide’s example, let x = { &String::from("1234") }.len();, worked in 2021 because the temporary String was extended to the end of the statement. In 2024 it is dropped at the end of the block, giving an error about a temporary dropped while borrowed. The guide’s fix is to bind the block’s result to a local first, as in let s = { &String::from("1234") };, so that it gets lifetime extension.
There is no semantics-preserving automatic rewrite for the tail expression change. The tail_expr_drop_order lint only points out values that may be affected and does not change your code, so you have to read the warnings and decide.
In short
- Locals in a block drop in reverse declaration order; struct fields drop in declaration order. If you have seen similar rules elsewhere, this is the same direction as Go’s defer, which runs calls in reverse order of registration, and Java’s try-with-resources, which closes resources in reverse declaration order.
let _ = expr;binds nothing. Ifexpris a temporary it is dropped at the end of the statement; if it is an existing variable it is not moved. To keep a value until the end of the scope, use a name like_guard.- Temporaries in a
matchscrutinee outlive thematch. - In the 2024 edition,
if lettemporaries drop beforeelse, and tail expression temporaries drop before locals. When moving a crate to the new edition, check the warnings from those two lints on any code that holds locks orRefCellborrows.
Scope and sources
This post is based on The Rust Reference and the Rust Edition Guide as checked on 2026-10-14. The Reference pages showed no version label; test links on the destructors page pointed at the 1.99.0 tag of rust-lang/rust. The Reference states that the exact rules for temporary lifetime extension are subject to change and describe current behaviour only. Lint default levels (deny/allow) are as listed in the rustc documentation on the check date.
Not covered: drops during panic unwinding, the drop order of variables a closure captures by move (unspecified in the Reference), ManuallyDrop and mem::forget, and async code. Example output is derived from the rules, not from a run.
The links below were checked on 2026-10-14.
- The Rust Reference, Destructors: https://doc.rust-lang.org/reference/destructors.html
- The Rust Reference, Patterns (Wildcard pattern): https://doc.rust-lang.org/reference/patterns.html
- The Rust Reference, Expressions (Temporaries): https://doc.rust-lang.org/reference/expressions.html
- Rust Edition Guide, Rust 2024: https://doc.rust-lang.org/edition-guide/rust-2024/index.html
- Rust Edition Guide, if let temporary scope: https://doc.rust-lang.org/edition-guide/rust-2024/temporary-if-let-scope.html
- Rust Edition Guide, Tail expression temporary scope: https://doc.rust-lang.org/edition-guide/rust-2024/temporary-tail-expr-scope.html
- rustc book, Deny-by-default lints: https://doc.rust-lang.org/rustc/lints/listing/deny-by-default.html
- rustc book, Allowed-by-default lints: https://doc.rust-lang.org/rustc/lints/listing/allowed-by-default.html
- Rust standard library, std::mem::drop: https://doc.rust-lang.org/std/mem/fn.drop.html
- The Cargo Book, The manifest format: https://doc.rust-lang.org/cargo/reference/manifest.html