Drop can be skipped
There are cases where destructors will not run.
// Copyright 2025 Google LLC
// SPDX-License-Identifier: Apache-2.0
#[derive(Debug)]
struct OwnedFd(i32);
impl Drop for OwnedFd {
fn drop(&mut self) {
println!("OwnedFd::drop() called with raw fd: {:?}", self.0);
}
}
impl Drop for TmpFile {
fn drop(&mut self) {
println!("TmpFile::drop() called with owned fd: {:?}", self.0);
// libc::unlink("/tmp/file")
// panic!("TmpFile::drop() panics");
}
}
#[derive(Debug)]
struct TmpFile(OwnedFd);
impl TmpFile {
fn open() -> Self {
Self(OwnedFd(2))
}
fn close(&self) {
panic!("TmpFile::close(): not implemented yet");
}
}
fn main() {
let owned_fd = OwnedFd(1);
let file = TmpFile::open();
std::process::exit(0);
// std::mem::forget(file);
// file.close();
let _ = owned_fd;
}
-
Drop is not guaranteed to always run. There is a number of cases when drop is skipped: the program can crash or exit, the value with the drop implementation can be leaked etc.
-
In the version that calls
std::process::exit,TmpFile::drop()is never run becauseexit()terminates the process immediately without any opportunity for adrop()method to be called.- You can prevent accidental use of
exitby denying theclippy::exitlint.
- You can prevent accidental use of
-
If you remove the
std::process::exit(0)line, eachdrop()method in this simple case will run in turn. -
Try uncommenting the
std::mem::forgetcall. What do you think will happen?mem::forget()takes ownership and “forgets” about the valuefilewithout running its destructorDrop::drop(). The destructor ofowned_fdis still run. -
Remove the
mem::forget()call, then uncomment thefile.close()call below it. What do you expect now?With the default
panic = "unwind"setting, the stack still unwinds and destructors run, even when the panic starts inmain.- With
panic = "abort"no destructors are run.
- With
-
As a last step, uncomment the
panic!insideTmpFile::drop()and run it. Ask the class: which destructors run before the abort?After a double panic, Rust no longer guarantees that remaining destructors will run:
- Some cleanup that was already in progress may still complete (for example, field destructors of the value currently being dropped),
- but anything scheduled later in the unwind path might be skipped entirely.
- This is why we say you cannot rely solely on
drop()for critical external cleanup, nor assume that a double panic aborts without running any further destructors.
-
Some languages forbid or restrict exceptions in destructors. Rust allows panicking in
Drop::drop, but it is almost never a good idea, since it can disrupt unwinding and lead to unpredictable cleanup. It is best avoided unless there is a very specific need, such as in the case of a drop bomb. -
Drop is suitable for cleaning up resources within the scope of a process, but it is not the right tool for providing hard guarantees that something happens outside of the process (e.g., on local disk, or in another service in a distributed system).
-
For example, deleting a temporary file in
drop()is fine in a toy example, but in a real program you would still need an external cleanup mechanism such as a temp file reaper. -
In contrast, we can rely on
drop()to unlock a mutex, since it is a process-local resource. Ifdrop()is skipped and the mutex is left locked, it has no lasting effects outside the process.