Drop, RAII & Drop Check
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Rust 1.99.0 and the 2024 edition, checked on 2026-10-06. When a value’s owner goes out of
scope, Rust runs its destructor: first the type’s own Drop::drop, if it has one, then the destructors of its fields.
This is deterministic and happens at a point you can predict from the source, which is what makes RAII — tying a
resource’s lifetime to an object’s lifetime — work for memory, files, locks, transactions and more. This page covers the
rules and the tools for when you need to deviate from them.
The Drop trait
struct TempExport {
path: std::path::PathBuf,
}
impl Drop for TempExport {
fn drop(&mut self) {
// Runs exactly once, when the value goes out of scope.
let _ = std::fs::remove_file(&self.path); // ignore errors: Drop cannot return them
println!("removed {}", self.path.display());
}
}
fn main() {
let path = std::env::temp_dir().join("shelf-export.json");
std::fs::write(&path, "[]").expect("write temp file");
{
let _export = TempExport { path };
println!("exporting...");
} // drop runs here
println!("done");
}
Rules worth knowing:
-
droptakes&mut self; it cannot fail or return a value, so errors during cleanup must be ignored, logged, or handled by an explicitclose(self) -> Result<..>method offered alongsideDrop. -
You cannot call
value.drop()directly (error E0040); callstd::mem::drop(value)— a function that simply takes ownership — to drop early. -
A type that implements
Dropcannot beCopy, and you cannot move fields out of it (E0509).
Drop order
The Reference specifies the order precisely:
-
Local variables are dropped in reverse order of declaration, at the end of their scope.
-
Fields of a struct are dropped in declaration order, after the struct’s own
drop; tuple, array andVecelements are dropped first to last. -
Function arguments are dropped after the function’s locals, in reverse order.
-
Temporaries — values created while evaluating an expression and not bound to a variable — are dropped at the end of the enclosing statement, except where temporary lifetime extension applies (
let x = &temp();keeps the temporary forx's scope). The 2024 edition changed the temporary scopes ofif letconditions and block tail expressions; see Control Flow.
struct Noisy(&'static str);
impl Drop for Noisy {
fn drop(&mut self) {
println!("drop {}", self.0);
}
}
struct Pair {
first: Noisy,
second: Noisy,
}
impl Drop for Pair {
fn drop(&mut self) {
println!("drop Pair (fields follow)");
}
}
fn make(name: &'static str) -> Noisy {
Noisy(name)
}
fn main() {
let _a = Noisy("a");
let _b = Noisy("b");
let _pair = Pair { first: Noisy("pair.first"), second: Noisy("pair.second") };
let _ = make("discarded"); // `_` binds nothing: dropped immediately
println!("len {}", make("temporary").0.len()); // temporary dropped at the end of this statement
let v = vec![Noisy("v[0]"), Noisy("v[1]")];
drop(v); // explicit early drop
println!("end of main");
}
// drop discarded / len 9 / drop temporary / drop v[0] / drop v[1] / end of main
// drop Pair (fields follow) / drop pair.first / drop pair.second / drop b / drop a
Source: The Rust Reference: Destructors
Partial moves and drop flags
If a value is moved out on some paths but not others, the compiler keeps a hidden drop flag to remember whether the
variable still needs dropping. After a partial move out of a struct (allowed only when it does not implement Drop),
the remaining fields are dropped at the end of the scope and the moved field is dropped by its new owner (Azam,
Advanced Rust, chapter 9, walks through these cases).
struct Noisy(&'static str);
impl Drop for Noisy {
fn drop(&mut self) {
println!("drop {}", self.0);
}
}
struct Loan {
book: Noisy,
member: Noisy,
}
fn main() {
let loan = Loan { book: Noisy("book"), member: Noisy("member") };
let book = loan.book; // partial move
if std::env::args().count() > 5 {
drop(book); // drop flag: maybe dropped here...
}
println!("end of main");
} // ...otherwise here; loan.member is dropped here too
Source: The Rust Reference: Destructors
RAII guards and scope guards
The standard library uses RAII for every resource: File closes on drop, MutexGuard unlocks, JoinHandle detaches,
Vec frees. The same pattern lets you build a scope guard that runs cleanup or a rollback on every exit path — including early returns and panics — the Rust replacement for finally:
struct Rollback<'a> {
log: &'a mut Vec<String>,
committed: bool,
}
impl Rollback<'_> {
fn commit(mut self) {
self.committed = true;
}
}
impl Drop for Rollback<'_> {
fn drop(&mut self) {
if !self.committed {
self.log.push("rolled back".into()); // runs on early return or panic
}
}
}
fn import(log: &mut Vec<String>, ok: bool) -> Result<(), String> {
let guard = Rollback { log, committed: false };
if !ok {
return Err("bad row".into()); // guard dropped: rollback
}
guard.commit();
Ok(())
}
fn main() {
let mut log = Vec::new();
let _ = import(&mut log, false);
let _ = import(&mut log, true);
println!("{log:?}"); // ["rolled back"]
}
Database transactions in sqlx follow exactly this pattern: an uncommitted Transaction rolls back when dropped (see
Axum: Migrations & Transactions).
The drop check
The drop checker ("dropck") makes sure that when a value is dropped, everything its destructor might touch is still
alive. For a generic type with a Drop impl, the compiler must assume drop could use the borrowed data inside it, so
that data must strictly outlive the value. This is usually invisible, but it explains some surprising errors:
struct Inspector<'a>(&'a String);
impl Drop for Inspector<'_> {
fn drop(&mut self) {
println!("inspecting {}", self.0); // drop *uses* the borrowed data
}
}
fn main() {
let (inspector, title);
title = String::from("Dune");
inspector = Inspector(&title); // error[E0597]: `title` does not live long enough
// Locals declared in one `let` are dropped in reverse order: `title` first, then `inspector`,
// whose Drop would read a freed String.
}
Source: The Rustonomicon: Drop Check
Without the Drop impl, the same code compiles: the compiler knows dropping a plain struct with a reference does not
read through it.
Vec<T> and Box<T> need to say "my destructor drops T but does not otherwise access borrowed data". The standard
library does that with the #[may_dangle] attribute on its Drop impls. #[may_dangle] requires the
dropck_eyepatch feature and is therefore nightly-only and unsafe; on stable you cannot write it, and custom
collections that wrap Vec get the right behaviour by containing one (and using PhantomData<T> to declare ownership
when they store raw pointers).
Controlling drops: drop, forget, ManuallyDrop, take and replace
| Tool | Use |
|---|---|
|
drop now, before the end of the scope (release a lock early, free memory) |
|
never run |
|
a wrapper whose contents are not dropped automatically; call |
|
move the value out and leave |
|
move the value out and leave |
|
move out of an |
use std::mem;
#[derive(Debug, Default)]
enum Loan {
#[default]
None,
Active { member: String },
Returned { member: String },
}
impl Loan {
// Change state through &mut self without cloning the String: take it out, build the new state.
fn give_back(&mut self) {
*self = match mem::take(self) {
Loan::Active { member } => Loan::Returned { member },
other => other,
};
}
}
fn main() {
let mut loan = Loan::Active { member: "alice".into() };
loan.give_back();
println!("{loan:?}"); // Returned { member: "alice" }
let mut queue = vec![1, 2, 3];
let drained = mem::replace(&mut queue, Vec::with_capacity(8));
println!("{drained:?} {queue:?}");
}
Panic safety, double panics and catch_unwind
When a panic unwinds, destructors run on the way up. Two consequences:
-
Panic safety (exception safety): code that temporarily breaks an invariant, then calls something that may panic, can leave a data structure in an inconsistent state that a destructor or a later user observes. Safe Rust cannot cause undefined behaviour this way, but
unsafecode must be careful (the Rustonomicon’s "Exception Safety" chapter shows the techniques).Mutexmarks itself poisoned when a thread panics while holding the lock, so other threads learn the data may be inconsistent. -
Double panics: if a destructor panics while the thread is already unwinding from a panic, the process aborts. Destructors should therefore never panic.
std::panic::catch_unwind runs a closure and turns a panic into an Err. It requires the closure to be UnwindSafe — a marker that warns when captured &mut or interior-mutable data might be observed in a broken state; wrap with
AssertUnwindSafe when you have checked it is fine. It is meant for FFI boundaries and thread-pool or server
infrastructure, not for ordinary error handling, and it does nothing when panics abort (panic = "abort").
use std::panic::{self, AssertUnwindSafe};
fn main() {
panic::set_hook(Box::new(|info| eprintln!("custom hook: {info}"))); // replace the default message
let mut processed = 0;
let result = panic::catch_unwind(AssertUnwindSafe(|| {
processed += 1;
if processed == 1 {
panic!("row 7 is corrupt");
}
}));
let _ = panic::take_hook(); // restore the default hook
println!("caught: {} after {processed} row(s)", result.is_err());
}
Leaking
Leaking — never running a destructor — is memory-safe in Rust, and therefore allowed in safe code: mem::forget,
Box::leak, Rc cycles. A consequence for unsafe code is that you may not rely on a destructor running for
soundness. That is why std::thread::scope (which guarantees joins) is a closure-based API, and why the old
thread::scoped API that relied on a guard’s destructor was removed before Rust 1.0.
Source: The Rustonomicon: Leaking
Self-referential types
A struct cannot safely contain a reference to one of its own fields, because moving the struct would invalidate the
reference. Async state machines need exactly that, which is the problem Pin solves; see
Pin & Unpin.
Where the books are outdated
Azam (Advanced Rust, chapter 9) covers the drop check, #[may_dangle], drop order and partial moves,
self-referential structs, mem::forget, ManuallyDrop, panic safety, double panics and catch_unwind. Note that
#[may_dangle] is a nightly-only attribute (feature dropck_eyepatch) and cannot be used on stable Rust, and that the
2024 edition changed the drop order of temporaries in if let conditions and tail expressions.
References
-
The Rust Programming Language: Running Code on Cleanup with the Drop Trait
-
std::mem (drop, forget, take, replace, ManuallyDrop)
-
std::panic (catch_unwind, UnwindSafe, set_hook)
-
Rust Design Patterns: RAII with guards (community)