Drop, RAII, and Resource Cleanup
Rust has no garbage collector and no finally block, yet files get closed, locks get released, and memory gets freed, all without you writing cleanup code at each early return. The mechanism is RAII (Resource Acquisition Is Initialization): a value owns a resource, and when the value goes out of scope, the Drop trait runs cleanup automatically. The MutexGuard from Concurrency in Practice and every Box/Vec/String in this series already rely on it. This tutorial makes the mechanism explicit: how Drop works, the order it runs in, and the cases where the automatic behavior needs steering.
The Drop Trait: Cleanup Tied to Scope
Implementing Drop gives a type a destructor, a drop method the compiler inserts a call to automatically when the value goes out of scope. You never call drop yourself; the compiler does, at exactly the right point.
struct TempFile {
path: String,
}
impl Drop for TempFile {
fn drop(&mut self) {
println!("deleting {}", self.path);
// std::fs::remove_file(&self.path).ok();
}
}
fn main() {
let _f = TempFile { path: "/tmp/scratch".into() };
println!("working...");
} // "deleting /tmp/scratch" runs here, automatically, as `_f` goes out of scope
The cleanup runs no matter how the scope exits, a normal return, an early return, a ? propagating an error, or a panic unwinding the stack. This is what makes RAII robust: there's no code path that can "forget" to release the resource, because the release is attached to the value's lifetime, not to any particular line you have to remember to write.
Drop Order: Reverse of Declaration
When a scope ends, local variables are dropped in reverse order of declaration, last declared, first dropped. Struct fields drop in declaration order (first field first). This matters whenever one resource depends on another still being alive during its own cleanup:
struct Noisy(&'static str);
impl Drop for Noisy {
fn drop(&mut self) {
println!("dropping {}", self.0);
}
}
fn main() {
let _first = Noisy("first");
let _second = Noisy("second");
}
// prints:
// dropping second
// dropping first
The reverse order is deliberate: a later variable may have been built using an earlier one, so the dependent (later) value is torn down before the thing it depended on. You rarely need to think about this, but when a cleanup ordering bug does appear, it's almost always because two resources were declared in an order that drops them backwards from how they depend on each other.
You Can't Call .drop(), but You Can drop(x)
Calling value.drop() directly is a compile error, if it were allowed, the value would still be in scope afterward and the compiler would also drop it at scope end, freeing the same resource twice. To drop something early on purpose, hand it to the free function std::mem::drop, which takes the value by value (moving it) so the compiler knows it's gone:
let data = load_huge_dataset();
process(&data);
drop(data); // free it now, before the long tail of the function runs
do_more_work_that_doesnt_need_data();
drop(data) is the idiomatic way to release a resource before its scope naturally ends, most commonly to release a lock early (drop(guard)) so other threads aren't blocked longer than necessary, or to free a large allocation before a subsequent memory-hungry operation. The function body is literally empty: it works purely by taking ownership, which triggers the normal end-of-scope drop immediately.
The Guard Pattern: RAII for Arbitrary Cleanup
Drop generalizes beyond memory to any paired setup/teardown: open/close, lock/unlock, begin/commit, increment/decrement. A "guard" is a small value whose only job is to run teardown in its drop, so the teardown can't be skipped:
struct SpanTimer {
label: &'static str,
start: std::time::Instant,
}
impl SpanTimer {
fn new(label: &'static str) -> Self {
SpanTimer { label, start: std::time::Instant::now() }
}
}
impl Drop for SpanTimer {
fn drop(&mut self) {
println!("{} took {:?}", self.label, self.start.elapsed());
}
}
fn handle_request() {
let _timer = SpanTimer::new("handle_request");
// ... any number of early returns or `?` propagations ...
} // the timing is recorded here regardless of how the function exited
This is exactly how MutexGuard, RefCell's Ref/RefMut, and database transaction guards work. Binding the guard to _timer (a named binding) rather than _ matters: a plain let _ = SpanTimer::new(...) drops the value immediately, on the same line, defeating the whole purpose. Use _timer (underscore-prefixed name) when you want the value kept alive to the end of scope without a "unused variable" warning.
Gotcha:
let _ = guard;andlet _guard = guard;look almost identical and behave oppositely. Bare_is not a binding, the value is dropped right there;_guardis a real binding that lives to end of scope. A lock or timer assigned tolet _ =silently does nothing, and there's no warning to tell you.
ManuallyDrop and mem::forget: Suppressing the Drop
Occasionally you need to prevent the automatic drop, most often at an FFI boundary where ownership of a resource is being handed to C code, which will free it, and running Rust's destructor too would be a double-free. std::mem::forget(x) consumes a value without running its destructor; ManuallyDrop<T> wraps a value so its drop never runs unless you explicitly invoke it.
use std::mem::ManuallyDrop;
let resource = ManuallyDrop::new(acquire_handle());
let raw_ptr = &*resource as *const _;
// hand raw_ptr to C, which now owns the cleanup;
// Rust will NOT run the destructor for `resource`
This is a sharp tool and a rare one, suppressing a drop means a resource leaks unless something else frees it, so it belongs almost exclusively in unsafe/FFI code (from Unsafe Rust Basics) where the ownership handoff is deliberate. In ordinary safe code, letting Drop run on its own is virtually always what you want; reaching for forget/ManuallyDrop is the exception that signals "ownership is crossing a boundary the compiler can't see."
Quick Reference
| You want | Use |
|---|---|
| Cleanup tied to scope exit | impl Drop for T |
| Release a value early | drop(x) (the free function) |
| Keep a guard alive to end of scope | let _name = guard; (named, not bare _) |
| Run a guard's teardown immediately | let _ = guard; (usually a bug — be sure) |
| Paired setup/teardown (lock, timer, txn) | the guard pattern (Drop on a small value) |
| Hand ownership to FFI, suppress drop | mem::forget / ManuallyDrop |
Key Takeaways
Dropattaches cleanup to a value's scope, run automatically by the compiler on every exit path (normal return,?, panic), which is why no code path can forget to release a resource.- Local variables drop in reverse declaration order (last in, first out); struct fields drop in declaration order. This ensures a dependent value is torn down before what it depends on.
- You can't call
.drop()directly (it would double-free); use the free functiondrop(x)to release a value early, commonly to release a lock or free a large allocation ahead of scope end. - The guard pattern uses
Dropfor arbitrary paired teardown (timers, locks, transactions). Bind the guard to a named_namevariable, not bare_, or it drops immediately instead of at scope end. mem::forgetandManuallyDropsuppress the automatic drop, a rare tool reserved mainly for FFI ownership handoffs; in safe code, lettingDroprun is almost always correct.