Drop trait runs cleanup code automatically when a value goes out of scope;drop(x) in stead of x.drop() when you want to release something early;Learn Rust Series):At the end of last episode I promised we would look at the other end of a value's life: not the moment it is born, but the precise, predictable instant it dies. Today we make good on that. Every value in Rust has a well-defined moment where it is cleaned up, and that moment is the point it goes out of scope. The Drop trait lets you hook into it to run your own teardown code, and because the timing is entirely deterministic, you get RAII, which stands for resource acquisition is initialization, the old C++ pattern that ties a resource's lifetime to a value's scope. This one idea is how Rust frees memory, closes files, unlocks mutexes, and flushes buffers, precisely and automatically, with no garbage collector wandering by later to decide when ;-)
Having said that, before we open the new topic we clear last episode's homework, as always.
Episode 19 was Deref, DerefMut, and deref coercion: how *b is rewritten into *(b.deref()), how the compiler chains deref calls until types line up, and why String coerces to &str and Vec<T> to &[T]. There were three exercises, and here is each one with full code you can paste and run.
Exercise 1 asked you to give MyBox<T> a Deref impl that also prints inside deref, then call a &str function with a MyBox<String> and count how many times coercion actually invokes deref for a single call:
use std::ops::Deref;
struct MyBox<T>(T);
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &T {
println!("deref called");
&self.0
}
}
fn takes_str(s: &str) {
println!("got: {s}");
}
fn main() {
let b = MyBox(String::from("Rust"));
takes_str(&b); // prints "deref called" exactly ONCE, then "got: Rust"
}
The number you see is one, and here is the why in a sentence: the coercion needs two hops, &MyBox<String> to &String and then &String to &str, but only the first hop uses our deref, which is the one that prints. The second hop runs the standard library's Deref for String, and that impl says nothing. So a two-step coercion produces a single line of output, because only one of the two deref implementations along the way happens to be talkative.
Exercise 2 wanted a Stack<T> wrapping a Vec<T> with both Deref and DerefMut, driven entirely through the inner Vec's methods without writing any of your own:
use std::ops::{Deref, DerefMut};
struct Stack<T>(Vec<T>);
impl<T> Deref for Stack<T> {
type Target = Vec<T>;
fn deref(&self) -> &Vec<T> { &self.0 }
}
impl<T> DerefMut for Stack<T> {
fn deref_mut(&mut self) -> &mut Vec<T> { &mut self.0 }
}
fn main() {
let mut s = Stack(Vec::new());
s.push(1);
s.push(2);
s.push(3);
let popped = s.pop();
println!("{:?} popped={:?} len={}", *s, popped, s.len());
// [1, 2] popped=Some(3) len=2
}
The key insight is that push and pop reach the inner Vec through DerefMut because they mutate, while len reaches it through the immutable Deref because it only reads. The Stack type wrote none of those three methods; it borrowed all of them from the Vec it wraps, which is exactly the "methods come along for free" behaviour we picked apart last time.
Exercise 3 asked for a function taking &[u8], then called with a Vec<u8> via &v, with a &Vec<u8>, and with an array reference, all three coercing to the slice:
fn sum(bytes: &[u8]) -> u32 {
bytes.iter().map(|&b| b as u32).sum()
}
fn main() {
let v: Vec<u8> = vec![1, 2, 3];
println!("{}", sum(&v)); // &Vec -> &[u8], via Vec's own Deref
let vr: &Vec<u8> = &v;
println!("{}", sum(vr)); // an existing &Vec coerces the same way
println!("{}", sum(&[4u8, 5, 6])); // &[u8; 3] -> &[u8]
}
The one-line why for the Vec case: Vec<u8> implements Deref<Target = [u8]>, so a &Vec<u8> coerces to &[u8] with no cost to the caller, which is precisely why a function should ask for the general &[u8] slice rather than the specific &Vec<u8>. Right, homework cleared, on to the far end of the value's life ;-)
Drop has a single method, drop, which the compiler calls automatically when a value is about to be destroyed. You never call it yourself; you just describe what cleanup should happen, and the compiler inserts the call for you at exactly the right spot:
struct Noisy { name: String }
impl Drop for Noisy {
fn drop(&mut self) {
println!("dropping {}", self.name);
}
}
fn main() {
let _a = Noisy { name: String::from("a") };
let _b = Noisy { name: String::from("b") };
println!("end of main reached");
}
Run this and the output is "end of main reached", then "dropping b", then "dropping a". You wrote no cleanup call anywhere; the compiler saw the closing brace of main, understood that _a and _b were both going out of scope there, and inserted the two drop invocations for you. Notice the signature: drop takes &mut self, not self. That is deliberate, because the value is not yet gone at the moment drop runs, it is only about to be, so the method gets a mutable borrow to do its work, and the actual deallocation of the fields happens right after your drop body finishes. This is the whole trait, one method, and it is the foundation for everything else in the episode.
Look again at that output. _b was dropped before _a, even though _a was declared first. This is not an accident, it is a guarantee: local variables are dropped in the reverse order of their declaration, last in first out, exactly like a stack:
struct Step(u32);
impl Drop for Step {
fn drop(&mut self) { println!("dropping step {}", self.0); }
}
fn main() {
let _first = Step(1);
let _second = Step(2);
let _third = Step(3);
println!("doing work");
// scope ends here, drops now run in reverse: 3, then 2, then 1
}
The reverse order is not arbitrary. Consider what would happen if _third had borrowed something out of _first: for that borrow to be valid, _third must be fully gone before _first is torn down, otherwise _third's destructor could touch a _first that no longer exists. Reverse-of-declaration is the only order that keeps later values, which may depend on earlier ones, alive until they are done. Fields inside a struct follow a different but equally sensible rule: the struct's own drop body runs first, and then its fields drop in declaration order, top to bottom:
struct Part(&'static str);
impl Drop for Part {
fn drop(&mut self) { println!("dropping part {}", self.0); }
}
struct Whole { first: Part, second: Part }
impl Drop for Whole {
fn drop(&mut self) { println!("dropping Whole first"); }
}
fn main() {
let _w = Whole { first: Part("first"), second: Part("second") };
// output: "dropping Whole first", then "dropping part first", then "dropping part second"
}
So the outer Whole::drop runs before either field is touched, and then the fields fall away in the order you wrote them. Local variables go last-in-first-out; struct fields go first-declared-first-out. Two rules, both there so that a value never outlives something it might be leaning on.
Sometimes you want to release a resource before the end of its scope, maybe to unlock a mutex early so another thread can proceed, or to close a file the moment you are done with it. You cannot call value.drop() directly to do this. Rust forbids it outright, because an explicit drop call followed by the automatic end-of-scope drop would run your cleanup twice, which for something like a memory free would be a genuine double-free bug. In stead you call the free function std::mem::drop, which is just drop in the prelude, and which takes the value by value:
struct Resource;
impl Drop for Resource {
fn drop(&mut self) { println!("resource released"); }
}
fn main() {
let r = Resource;
println!("before explicit drop");
drop(r); // r is MOVED into drop(), whose body is literally empty
println!("after explicit drop");
// r.drop(); // would NOT compile: "explicit use of destructor method is not allowed"
}
The output is "before explicit drop", then "resource released", then "after explicit drop". The trick is delightfully simple once you see it: std::mem::drop is a function with an empty body that takes ownership of whatever you hand it. Because drop(r) moves r into the function, r is gone from main the instant the call returns, and the value's destructor fires at the end of that tiny function instead of at the end of main. There is no special compiler magic here, just ownership doing its ordinary job: move a value somewhere, and it gets cleaned up wherever it landed. And because the move leaves main with no r anymore, the compiler knows not to drop it again at the closing brace.
The real power of Drop is building guards: small values whose entire job is to hold a resource and release it on the way out. A timer that reports how long a scope took is a clean, self-contained example you can run right now:
use std::time::Instant;
struct Timer { label: String, start: Instant }
impl Timer {
fn new(label: &str) -> Timer {
Timer { label: label.to_string(), start: Instant::now() }
}
}
impl Drop for Timer {
fn drop(&mut self) {
println!("{} took {:?}", self.label, self.start.elapsed());
}
}
fn main() {
let _timer = Timer::new("main");
println!("doing some work");
// when _timer drops at the end of main, it prints the elapsed time on its own
}
Create the timer, do your work, and then forget about it entirely; the elapsed time prints automatically when the scope ends. You never have to remember to stop it, and here is the part that matters most: it prints even if you return early from the middle of the function, and even if a panic unwinds straight through the scope. This is exactly how a MutexGuard unlocks its mutex, how a File closes its handle, and how a database transaction rolls itself back if you drop it without committing. The pattern is always the same: acquire the resource when you build the value, release it in drop, and let scope do the bookkeeping.
Because drop is tied to scope, an inner block cleans up at its own closing brace, which lets you bound a resource's life as tightly as you like:
struct Guard(&'static str);
impl Drop for Guard {
fn drop(&mut self) { println!("releasing {}", self.0); }
}
fn main() {
println!("outer start");
{
let _g = Guard("inner");
println!("inside the block");
} // _g is released HERE, before the next line runs
println!("outer end");
}
The output is "outer start", "inside the block", "releasing inner", "outer end", in that order. The guard is gone before outer end ever prints, because the inner { ... } block is its whole world. Now for the claim I keep making about panics, let us actually prove it. When a Rust program panics, by default it unwinds the stack, and unwinding runs the destructor of every live value on the way out. That means your cleanup is not skipped just because something went wrong, which is the entire reason RAII is safe to rely on:
struct Guard(&'static str);
impl Drop for Guard {
fn drop(&mut self) { println!("releasing {}", self.0); }
}
fn risky() {
let _g = Guard("held during risky()");
println!("about to panic");
panic!("something went wrong");
}
fn main() {
let result = std::panic::catch_unwind(risky);
println!("main survived, risky panicked: {}", result.is_err());
}
The output shows "about to panic", then "releasing held during risky()" (that is the destructor firing during the unwind), then the panic message on stderr, and finally "main survived, risky panicked: true". Read that order carefully, because it is the whole point: the panic did not leak the guard's resource. Rust unwound the stack, and on its way out it ran Guard::drop for _g just as faithfully as it would have on a normal return. In a garbage-collected language you would reach for try/finally to get this guarantee, and you would have to remember to write it at every call site. RAII gives it to you for free, everywhere, because it is built into what "a value going out of scope" means.
A lot of you arrive at this series from Python (as I did, having taught it for years), so it is worth seeing where deterministic destruction has an analogue and where it genuinely does not. Python has __del__, which looks like drop, but it is a trap: it runs whenever the garbage collector happens to reclaim the object, which might be immediately, might be at the end of the program, and might be never for objects caught in a reference cycle. So Python's real answer to RAII is not __del__ at all, it is the context manager and the with statement:
class Timer:
def __enter__(self):
print("timer started")
return self
def __exit__(self, exc_type, exc_val, tb):
print("timer stopped") # runs even if the block raises
with Timer():
print("doing some work")
# prints: timer started, doing some work, timer stopped
__exit__ is Python's drop, and it fires deterministically at the end of the with block, even when an exception tears through it, which is the direct parallel to Rust running drop during a panic unwind. The difference is that Python makes the scope explicit with with, and only for objects you opted in, whereas Rust applies the same guarantee to every value automatically, no keyword required. You get RAII in Python only where you remember to ask for it; you get it in Rust always.
Go takes yet another route with the defer keyword, which schedules a call to run when the surrounding function returns:
package main
import "fmt"
func work() {
defer fmt.Println("cleanup runs on the way out")
fmt.Println("doing some work")
}
func main() {
work()
// prints: doing some work, then cleanup runs on the way out
}
defer is closer to Rust than Python's __del__ is, because it is deterministic and even runs during a panic. But notice the difference in granularity: defer is tied to the whole function, not to a nested scope, and you have to write a defer line for each cleanup by hand at the point you acquire the resource. Rust ties cleanup to the value and to whatever scope holds it, down to an inner { ... } block, and you write the teardown once in the type's Drop impl rather than at every use site. Three languages, three takes on the same problem: Python asks politely with with, Go schedules with defer, and Rust bakes it into the type system so you cannot forget.
One piece of judgement before the summary, because Drop is tempting to over-engineer. The trait is meant for releasing resources: memory, file handles, locks, sockets, timers, transactions. It is not a general "run this code later" mechanism, and it comes with a few sharp edges you should know about. You cannot move a field out of self inside drop, because the value is only borrowed there, not owned. You should keep drop bodies fast and non-panicking, because a panic inside a drop that is itself running during an unwind will abort the whole program. And you almost never implement Drop on a plain data type that only holds i32s and Strings, because those already clean themselves up perfectly well and adding a drop buys you nothing but a type that can no longer be copied. The rule of thumb is the same one I keep coming back to: reach for Drop when your type genuinely owns a resource that needs an explicit release, and leave it alone otherwise. The standard library follows this to the letter, which is why Box, File, MutexGuard, and Vec all implement it, and a bare Point { x, y } does not.
Drop has one method, drop(&mut self), which the compiler calls automatically at the end of a value's scope; you never call it directly, and it borrows self because the value is only about to be destroyed, not gone yet.drop runs first and then its fields drop in declaration order; both rules exist so a value never outlives something it might depend on.std::mem::drop(x), which simply moves x into an empty-bodied function so its destructor fires there; x.drop() is forbidden precisely to prevent a double drop.drop, and the release still happens on early returns and even during a panic unwind, which is what makes it safe to lean on.with/__exit__ and Go through defer, but Rust applies it to every value automatically and lets you write the teardown once in the type; use Drop for real resources, keep the body fast and panic-free, and skip it for plain data.The thread across these last few episodes has been Rust handing you one sharp, single-purpose trait at a time: associated types, operators, the pointer relationship, and now the exact moment a value is cleaned up. Next time we step away from the lifecycle of a value and look at how one type turns into another, the small idiomatic conversions that Rust threads through almost every real program, but one thing at a time ;-)
Three exercises, gentle to chewier as always. Full solutions open the next episode, so have a real go first, because typing this stuff yourself is where it actually sticks.
Noisy values in main, but wrap the middle one in its own inner { ... } block, and predict the drop order before you run it. Confirm from the output that the inner value drops at the inner brace, before the two outer ones fall away in reverse order at the end of main.FileGuard struct that prints "opening" in a new constructor and "closing" in its Drop impl, then create one, do a little println! work, and confirm the two messages bracket your work exactly. As a bonus, drop() it explicitly halfway through and watch "closing" move earlier.Drop impl, and also give the struct itself a Drop impl that prints. Run it and confirm the struct's own drop prints first, then the three fields drop top-to-bottom in declaration order, the opposite of how local variables behave.De groeten, en tot de volgende keer! ;-)