Drop guards and custom cleanup

Drop is the trait with one method, and the compiler calls that method when a value goes out of scope: at the end of a block, on an early return, on a panic that unwinds, or when the value is moved into a call that ends. The method is the place for a lock, a file or a counter, and it runs without a call in the source.

This lesson writes a type whose destructor prints, shows the order the drops run in, uses a guard that closes on an early return, and removes a temporary file from a destructor. The last section is the compiler error for calling the destructor by hand.

A type with a destructor

The trait is small: one method that takes &mut self and returns nothing. A type that implements it cannot be Copy, because a value that is duplicated would be released twice, and the compiler refuses that combination.

The program creates three values with the same destructor. The print at the end of main runs before any destructor, which shows that the drops happen on the way out of the scope.

struct Label(&'static str); impl Drop for Label { fn drop(&mut self) { println!("drop {}", self.0); } } fn main() { let first = Label("first"); let second = Label("second"); println!("end of main"); }

rustc --edition 2024 label.rs
./label

end of main
drop second
drop first

The order is the reverse of the declarations, which is the rule C++ also uses for locals. The lesson on Drop, scope and RAII introduces the trait with a value whose release is observable.

The order of the drops

Locals drop in reverse order of declaration, and the fields of a struct drop in declaration order after the struct's own destructor. The two rules are enough to predict a log, and a struct field that must be released after another is placed accordingly.

The program builds a struct of two labels and one local label. The print reads all three, so nothing is unused, and the drops that follow show the two orders side by side.

struct Label(&'static str); impl Drop for Label { fn drop(&mut self) { println!("drop {}", self.0); } } struct Pair { first: Label, second: Label, } fn main() { let pair = Pair { first: Label("first"), second: Label("second"), }; let local = Label("local"); println!("{} {} {}", pair.first.0, pair.second.0, local.0); }

rustc --edition 2024 order.rs
./order

first second local
drop local
drop first
drop second

The local goes first because it was declared last, then the struct, whose fields go in the order they are written. A struct with its own Drop runs that method before the fields, so a handle that borrows a field can still use it there. The struct lesson covers field initialisation.

A guard and an early return

A guard is a value that exists only to run a destructor. Binding it to a name at the top of a function makes the cleanup run on every exit path: the normal end, the early return and the unwinding panic.

The program calls the same function twice, once with an early return and once without. The guard closes in both cases, before the caller prints the result.

struct Guard(&'static str); impl Drop for Guard { fn drop(&mut self) { println!("closing {}", self.0); } } fn work(fail: bool) -> i32 { let _guard = Guard("handle"); if fail { return -1; } 42 } fn main() { println!("{}", work(true)); println!("{}", work(false)); }

rustc --edition 2024 guard.rs
./guard

closing handle
-1
closing handle
42

The underscore in _guard keeps the value alive for the whole function; a bare _ would drop it at once. That difference is the one trap of the pattern, and it is the reason the binding is named.

A guard that removes a file

The same shape closes a real resource. The guard below owns a path, creates the file when it is built and deletes it in the destructor, so no code path can leave the file behind.

The program creates the guard and checks that the file exists while the guard is alive. The removal line is printed by the destructor at the end of main, and the check afterwards finds nothing.

use std::fs; use std::path::PathBuf; struct TempPath(PathBuf); impl TempPath { fn new(path: &str) -> Self { fs::write(path, "temporary").unwrap(); TempPath(PathBuf::from(path)) } } impl Drop for TempPath { fn drop(&mut self) { fs::remove_file(&self.0).unwrap(); println!("removed {}", self.0.display()); } } fn main() { let temp = TempPath::new("/tmp/drop_demo.txt"); println!("exists {}", temp.0.exists()); }

rustc --edition 2024 temp.rs
./temp
ls /tmp/drop_demo.txt

exists true
removed /tmp/drop_demo.txt
ls: /tmp/drop_demo.txt: No such file or directory

A destructor cannot report an error, so the unwrap here is a decision: a failed removal panics inside the drop, and a panic in a destructor during another panic aborts the process. A library guard usually ignores the error or records it, which is why File flushes on drop and leaves the error to an explicit call. The lesson on text files shows the explicit form.

A panic also runs the destructors, as long as the build unwinds: the guard closes the handle on the way out of a failed assertion exactly as it does on a return. That is why a guard is preferable to a close call at the end of a function, which a panic would skip. Two things break the promise: std::mem::forget, which takes the value and never drops it, and a process that aborts before the unwind.

A destructor called by hand

There is no .drop() method to call: the destructor runs at the end of the scope, and a second run would release the value twice. The compiler reports E0040 and suggests the free function that takes the value by move.

The program calls the method on a guard. The suggestion in the error is the fix, and it is a call to drop with the value as an argument.

struct Guard(&'static str); impl Drop for Guard { fn drop(&mut self) { println!("closing {}", self.0); } } fn main() { let guard = Guard("handle"); guard.drop(); }

rustc --edition 2024 call_err.rs

error[E0040]: explicit use of destructor method
--> call_err.rs:11:11
|
11 | guard.drop();
| ^^^^ explicit destructor calls not allowed
|
help: consider using `drop` function
|
11 - guard.drop();
11 + drop(guard);
|

The free function moves the value into itself and lets the scope end there, which is why it can run the destructor exactly once. The lesson on uninitialised memory uses the same call to end a value early, and ManuallyDrop for the case where it must not run at all.

Exercises

  1. Write a guard that prints its name and build a struct of three of them; predict the log before you run it.
  2. Add a Drop to a struct that owns a Vec and print the length from the destructor.
  3. Make a guard restore a value in its destructor after a function changes it.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. When does the compiler call Drop::drop?

    Answer

    When the value leaves its scope

    The call is inserted at every point where the value goes out of scope.

  2. Why can a type with Drop not be Copy?

    Answer

    A copy would be released twice

    Two values would own the same resource and both would run the destructor.

  3. In which order do the locals of a scope drop?

    Answer

    Reverse order of declaration

    The last value created is the first released, like a stack.

  4. What does a guard bound at the top of a function guarantee?

    Answer

    The cleanup runs on every exit path

    The value lives until the function returns, whatever the path.

  5. What does this program print? struct Label(&'static str); impl Drop for Label { fn drop(&mut self) { println!("{}", self.0); } } fn main() { let first = Label("a"); let second = Label("b"); }

    Answer

    b / a

    The second local is released first, and each destructor prints one letter.

  6. What does this program print? struct Pair { left: Box<i32>, right: Box<i32>, } impl Drop for Pair { fn drop(&mut self) { println!("pair {} {}", self.left, self.right); } } fn main() { let pair = Pair { left: Box::new(1), right: Box::new(2) }; }

    Answer

    pair 1 2

    The destructor of the struct reads both fields before they are dropped.

  7. What does this program print? struct Guard; impl Drop for Guard { fn drop(&mut self) { println!("close"); } } fn run() -> i32 { let _guard = Guard; return 7; } fn main() { println!("{}", run()); }

    Answer

    close / 7

    The guard drops when run returns, before main prints the result.

  8. What does this program print? struct Guard; impl Drop for Guard { fn drop(&mut self) { println!("close"); } } fn main() { let _ = Guard; println!("after"); }

    Answer

    close / after

    A value bound to the underscore pattern is dropped at once, before the next statement.

  9. What is the first line of the compiler output for this program? struct Guard; impl Drop for Guard { fn drop(&mut self) { println!("close"); } } fn main() { let guard = Guard; guard.drop(); }

    Answer

    error[E0040]: explicit use of destructor method

    The destructor runs at the end of the scope, and the free function is the way to end a value early.

  10. What is the first line of the compiler output for this program? #[derive(Clone, Copy)] struct Guard; impl Drop for Guard { fn drop(&mut self) {} } fn main() { let guard = Guard; let copy = guard; println!("{}", std::mem::size_of_val(&copy)); }

    Answer

    error[E0184]: the trait Copy cannot be implemented for this type; the type has a destructor

    A type with a destructor cannot also be copied, because the destructor would run twice.

The code on this page was built with rustc 1.96.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev