The Drop Trait & RAII
Reviewed & published by Brayan K
The Drop trait lets a type run cleanup code automatically the instant its value goes out of scope, giving Rust deterministic resource management without a garbage collector.
Part of the free Rust course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
In this lesson you'll implement Drop, learn the exact drop order, drop a value early with std::mem::drop, and see the RAII guard pattern that powers things like MutexGuard.
What You'll Learn in This Lesson
- How to implement the Drop trait
- When cleanup runs — at the end of a scope
- Why values drop in reverse declaration order
- Dropping a value early with std::mem::drop
- The RAII guard pattern (like MutexGuard)
- Why Rust needs no garbage collector
1️⃣ Implementing Drop
To run code when a value is destroyed, implement the Drop trait, which has a single method: fn drop(&mut self). Rust calls it automatically when the value goes out of scope — you never call it yourself. This is the hook for closing files, releasing locks, flushing buffers, or logging.
// Implementing Drop lets you run cleanup code automatically
// when a value goes out of scope — no destructor call needed.
struct Resource {
name: String,
}
impl Drop for Resource {
fn drop(&mut self) {
// This runs the moment self leaves scope.
println!("Cleaning up '{}'", self.name);
}
}
fn main() {
println!("start of main");
let _a = Resource { name: String::from("database") };
{
let _b = Resource { name: String::from("inner file") };
println!("inside inner block");
} // _b is dropped HERE, at the end of the inner block
println!("back in main");
} // _a is dropped HERE, at the end of mainNotice the inner _b is cleaned up as soon as its block closes, while _a waits until the end of main. Cleanup is precisely scoped, not deferred to some later collection pass.
2️⃣ Drop Order & Dropping Early
Within a scope, values are dropped in the reverse of their declaration order — the last one created is the first to be destroyed, exactly like unwinding a stack. This guarantees that a value can safely rely on things declared before it still being alive during its own cleanup.
struct Tagged(&'static str);
impl Drop for Tagged {
fn drop(&mut self) {
println!("dropping {}", self.0);
}
}
fn main() {
// Three values declared in order: first, second, third.
let _first = Tagged("first");
let _second = Tagged("second");
let _third = Tagged("third");
println!("end of main reached");
// Variables drop in REVERSE order of declaration:
// third, then second, then first (like unwinding a stack).
}If you need to release a resource before the end of its scope, hand it to std::mem::drop. That moves the value in and lets it go out of scope immediately, running its destructor right there.
struct Guard(&'static str);
impl Drop for Guard {
fn drop(&mut self) {
println!("releasing {}", self.0);
}
}
fn main() {
let a = Guard("A");
let b = Guard("B");
// std::mem::drop runs the destructor EARLY, right now,
// instead of waiting for the end of the scope.
println!("about to drop A early");
std::mem::drop(a); // releases A immediately
println!("A is gone; doing more work...");
// b still drops normally at the end of main.
let _ = &b;
}3️⃣ The RAII Guard Pattern
RAII (Resource Acquisition Is Initialization) is the idea that a resource's lifetime is bound to a value. A guard acquires the resource when constructed and releases it in its Drop. This is exactly how the standard library's MutexGuard works: locking returns a guard, and the lock is released automatically when the guard drops — even on an early return or a panic.
// A real-world RAII pattern: a guard that "locks" on creation
// and automatically "unlocks" when it goes out of scope.
struct LockGuard {
label: String,
}
impl LockGuard {
fn acquire(label: &str) -> LockGuard {
println!("LOCK acquired by {}", label);
LockGuard { label: label.to_string() }
}
}
impl Drop for LockGuard {
fn drop(&mut self) {
// Cleanup is tied to the value's lifetime — impossible to forget.
println!("UNLOCK released by {}", self.label);
}
}
fn critical_section() {
let _guard = LockGuard::acquire("worker");
println!(" ...doing protected work...");
// No explicit unlock: _guard drops at the end, releasing the lock,
// even if the function returned early or panicked.
}
fn main() {
critical_section();
println!("back in main, lock already released");
}Because cleanup is tied to the guard's lifetime, you can't forget to release the lock — there is no matching unlock() call to miss. This is why Rust does not need a garbage collector: ownership tells the compiler exactly when every value dies, and Drop handles the rest deterministically.
Your turn. Fill in the blank marked ___, then run it.
// 🎯 YOUR TURN — implement Drop so the timer announces it stopped.
struct Timer {
label: String,
}
impl ___ for Timer { // 👉 the trait that adds cleanup
fn drop(&mut self) {
println!("Timer '{}' stopped", self.label);
}
}
fn main() {
let _t = Timer { label: String::from("load") };
println!("doing work...");
} // _t is dropped here, printing the stop message
// ✅ Expected output:
// doing work...
// Timer 'load' stoppedMini-Challenge: Self-Closing Connections
Build a Connection type that opens on creation and closes itself via Drop. Mind the reverse drop order. Run it with cargo run and check the output.
// 🎯 MINI-CHALLENGE: connection pool cleanup.
//
// 1. Define struct Connection { id: u32 }.
// 2. Give it an associated function open(id: u32) -> Connection that
// prints "opened connection <id>" and returns the connection.
// 3. impl Drop for Connection so it prints "closed connection <id>".
// 4. In main, open connection 1 then 2, print "both connections in use".
//
// Remember: values drop in REVERSE declaration order, so connection 2
// closes before connection 1.
//
// ✅ Expected output:
// opened connection 1
// opened connection 2
// both connections in use
// closed connection 2
// closed connection 1
fn main() {
// your code here
}Common Errors (and the fix)
- ❌ Calling value.drop() directly. ✅ Not allowed — use std::mem::drop(value) to drop early.
- ❌ Expecting drop in declaration order. ✅ It is the reverse of declaration order within a scope.
- ❌ Trying to move a field out in drop. ✅ drop takes &mut self, so you can read and mutate fields but not move them out wholesale.
- ❌ A type can't be both Copy and Drop. ✅ If a type needs custom cleanup it can't be trivially copyable — that is by design.
Pro Tips
- 💡 Let Drop do the work. Wrap resources in a type with a Drop impl instead of remembering to clean up by hand.
- 💡 Guards are scopes. To release a lock sooner, drop the guard early with std::mem::drop or a tighter { } block.
- 💡 Keep drop simple. Avoid panicking inside drop; a panic during unwinding can abort the whole program.
📋 Quick Reference — Drop & RAII
| Concept | Detail |
|---|---|
| Cleanup hook | fn drop(&mut self) |
| When it runs | End of owner's scope |
| Order | Reverse of declaration |
| Drop early | std::mem::drop(v) |
| RAII guard | Acquire on new, release on drop |
🎉 Lesson Complete!
- ✅ Drop runs cleanup automatically when a value leaves scope
- ✅ Values drop in reverse declaration order within a scope
- ✅ std::mem::drop releases a value early; you can't call .drop() yourself
- ✅ RAII guards tie a resource's lifetime to a value — cleanup can't be forgotten
Practice quiz
What is the single method of the Drop trait?
- fn close(self)
- fn free()
- fn drop(&mut self)
- fn dispose(&self)
Answer: fn drop(&mut self). Drop has one method, fn drop(&mut self), which Rust calls automatically at end of scope.
When does a value's Drop run?
- When its owner goes out of scope
- Immediately after creation
- Only on program exit
- When you call .free()
Answer: When its owner goes out of scope. A value is dropped the moment its owner leaves scope, typically at the closing brace of its block.
What does RAII stand for?
- Resource Allocation In Iteration
- Random Access Inline Indexing
- Reference And Index Invariant
- Resource Acquisition Is Initialization
Answer: Resource Acquisition Is Initialization. RAII = Resource Acquisition Is Initialization: a resource's lifetime is tied to a value's lifetime.
Within a scope, in what order are values dropped?
- Declaration order
- Reverse of declaration order
- Alphabetical order
- Random order
Answer: Reverse of declaration order. Values drop in reverse of declaration order, like unwinding a stack.
Three values _first, _second, _third are declared in that order. Which drops first?
- _third
- _first
- _second
- They drop simultaneously
Answer: _third. Reverse declaration order means _third drops first, then _second, then _first.
How do you drop a value early, before its scope ends?
- value.drop()
- free(value)
- std::mem::drop(value)
- delete value
Answer: std::mem::drop(value). std::mem::drop(value) takes ownership and runs the destructor immediately.
Why can't you call value.drop() directly?
- It's too slow
- Rust forbids it to prevent dropping a value twice (double free)
- drop() is private
- It needs a feature flag
Answer: Rust forbids it to prevent dropping a value twice (double free). Calling .drop() directly is a compile error so a value is never cleaned up twice.
In a Connection type with Drop, if connection 1 then connection 2 are opened in main, which closes first?
- Connection 1
- Both at once
- Neither closes
- Connection 2
Answer: Connection 2. Reverse drop order means connection 2 (declared last) closes before connection 1.
Why does Rust not need a garbage collector?
- It never frees memory
- Ownership plus Drop give deterministic, compile-time-tracked cleanup
- It uses reference counting only
- It runs a collector thread
Answer: Ownership plus Drop give deterministic, compile-time-tracked cleanup. Each value has one owner whose scope end is known at compile time, so cleanup is inserted automatically.
Which combination is NOT allowed for a single type?
- Both Debug and Clone
- Both Clone and Drop
- Both Copy and Drop
- Both Debug and Drop
Answer: Both Copy and Drop. A type can't be both Copy and Drop: needing custom cleanup means it can't be trivially copyable.
Continue this course
- Previous: Declarative Macros (macro_rules!)
- Next: The Newtype Pattern & Type Aliases — Add type safety with wrapper structs and the orphan-rule workaround
- Quick reference: Rust cheat sheet
Frequently asked questions
What is RAII and how does Rust use it?
RAII stands for Resource Acquisition Is Initialization. The idea is that you tie a resource — a file handle, a lock, a network connection, a chunk of heap memory — to the lifetime of a value. The resource is acquired when the value is created and released automatically when the value is destroyed. Rust uses RAII everywhere: when a value owning a resource goes out of scope, Rust runs its Drop implementation (if any) and frees what it owns. This is why you never call free in Rust and rarely call close explicitly — the type system guarantees cleanup happens.
When exactly does Drop run?
A value is dropped the moment its owner goes out of scope. For a variable, that is the end of the block it was declared in, the closing brace. If a value is moved out of a variable, it is dropped wherever its new owner goes out of scope instead. Within a single scope, values are dropped in the reverse of their declaration order, like unwinding a stack. Dropping also happens during a panic as the stack unwinds, which is what makes RAII guards reliable even when something goes wrong.
How do I drop a value early, before the end of its scope?
Call std::mem::drop(value), passing the value by move. This takes ownership and lets it go out of scope immediately, running its Drop implementation right there. You cannot call the drop method directly (value.drop()) — Rust forbids it to prevent a value from being cleaned up twice, which would lead to a double free. The free function std::mem::drop is the safe, idiomatic way to release something early, for example to unlock a mutex before doing slow unrelated work.
Why does Rust not need a garbage collector?
Because ownership plus Drop gives deterministic, compile-time-tracked cleanup. Each value has a single owner, and the compiler knows exactly where that owner's scope ends, so it can insert the cleanup code at compile time. There is no background collector scanning memory at runtime and no unpredictable pauses. The result is that memory and other resources are freed promptly and predictably, with the safety of a managed language but the performance and control of a systems language. Drop is the mechanism that makes this automatic.