Drop, RAII & Drop Check

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Rust 1.99.0 and the 2024 edition, checked on 2026-10-06. When a value’s owner goes out of scope, Rust runs its destructor: first the type’s own Drop::drop, if it has one, then the destructors of its fields. This is deterministic and happens at a point you can predict from the source, which is what makes RAII — tying a resource’s lifetime to an object’s lifetime — work for memory, files, locks, transactions and more. This page covers the rules and the tools for when you need to deviate from them.

The Drop trait

struct TempExport {
    path: std::path::PathBuf,
}

impl Drop for TempExport {
    fn drop(&mut self) {
        // Runs exactly once, when the value goes out of scope.
        let _ = std::fs::remove_file(&self.path); // ignore errors: Drop cannot return them
        println!("removed {}", self.path.display());
    }
}

fn main() {
    let path = std::env::temp_dir().join("shelf-export.json");
    std::fs::write(&path, "[]").expect("write temp file");
    {
        let _export = TempExport { path };
        println!("exporting...");
    } // drop runs here
    println!("done");
}

Rules worth knowing:

  • drop takes &mut self; it cannot fail or return a value, so errors during cleanup must be ignored, logged, or handled by an explicit close(self) -> Result<..> method offered alongside Drop.

  • You cannot call value.drop() directly (error E0040); call std::mem::drop(value) — a function that simply takes ownership — to drop early.

  • A type that implements Drop cannot be Copy, and you cannot move fields out of it (E0509).

Drop order

The Reference specifies the order precisely:

  • Local variables are dropped in reverse order of declaration, at the end of their scope.

  • Fields of a struct are dropped in declaration order, after the struct’s own drop; tuple, array and Vec elements are dropped first to last.

  • Function arguments are dropped after the function’s locals, in reverse order.

  • Temporaries — values created while evaluating an expression and not bound to a variable — are dropped at the end of the enclosing statement, except where temporary lifetime extension applies (let x = &temp(); keeps the temporary for x's scope). The 2024 edition changed the temporary scopes of if let conditions and block tail expressions; see Control Flow.

struct Noisy(&'static str);

impl Drop for Noisy {
    fn drop(&mut self) {
        println!("drop {}", self.0);
    }
}

struct Pair {
    first: Noisy,
    second: Noisy,
}

impl Drop for Pair {
    fn drop(&mut self) {
        println!("drop Pair (fields follow)");
    }
}

fn make(name: &'static str) -> Noisy {
    Noisy(name)
}

fn main() {
    let _a = Noisy("a");
    let _b = Noisy("b");
    let _pair = Pair { first: Noisy("pair.first"), second: Noisy("pair.second") };
    let _ = make("discarded");                  // `_` binds nothing: dropped immediately
    println!("len {}", make("temporary").0.len()); // temporary dropped at the end of this statement
    let v = vec![Noisy("v[0]"), Noisy("v[1]")];
    drop(v);                                     // explicit early drop
    println!("end of main");
}
// drop discarded / len 9 / drop temporary / drop v[0] / drop v[1] / end of main
// drop Pair (fields follow) / drop pair.first / drop pair.second / drop b / drop a

Partial moves and drop flags

If a value is moved out on some paths but not others, the compiler keeps a hidden drop flag to remember whether the variable still needs dropping. After a partial move out of a struct (allowed only when it does not implement Drop), the remaining fields are dropped at the end of the scope and the moved field is dropped by its new owner (Azam, Advanced Rust, chapter 9, walks through these cases).

struct Noisy(&'static str);

impl Drop for Noisy {
    fn drop(&mut self) {
        println!("drop {}", self.0);
    }
}

struct Loan {
    book: Noisy,
    member: Noisy,
}

fn main() {
    let loan = Loan { book: Noisy("book"), member: Noisy("member") };
    let book = loan.book;           // partial move
    if std::env::args().count() > 5 {
        drop(book);                 // drop flag: maybe dropped here...
    }
    println!("end of main");
} // ...otherwise here; loan.member is dropped here too

RAII guards and scope guards

The standard library uses RAII for every resource: File closes on drop, MutexGuard unlocks, JoinHandle detaches, Vec frees. The same pattern lets you build a scope guard that runs cleanup or a rollback on every exit path — including early returns and panics — the Rust replacement for finally:

struct Rollback<'a> {
    log: &'a mut Vec<String>,
    committed: bool,
}

impl Rollback<'_> {
    fn commit(mut self) {
        self.committed = true;
    }
}

impl Drop for Rollback<'_> {
    fn drop(&mut self) {
        if !self.committed {
            self.log.push("rolled back".into()); // runs on early return or panic
        }
    }
}

fn import(log: &mut Vec<String>, ok: bool) -> Result<(), String> {
    let guard = Rollback { log, committed: false };
    if !ok {
        return Err("bad row".into()); // guard dropped: rollback
    }
    guard.commit();
    Ok(())
}

fn main() {
    let mut log = Vec::new();
    let _ = import(&mut log, false);
    let _ = import(&mut log, true);
    println!("{log:?}"); // ["rolled back"]
}

Database transactions in sqlx follow exactly this pattern: an uncommitted Transaction rolls back when dropped (see Axum: Migrations & Transactions).

The drop check

The drop checker ("dropck") makes sure that when a value is dropped, everything its destructor might touch is still alive. For a generic type with a Drop impl, the compiler must assume drop could use the borrowed data inside it, so that data must strictly outlive the value. This is usually invisible, but it explains some surprising errors:

struct Inspector<'a>(&'a String);

impl Drop for Inspector<'_> {
    fn drop(&mut self) {
        println!("inspecting {}", self.0); // drop *uses* the borrowed data
    }
}

fn main() {
    let (inspector, title);
    title = String::from("Dune");
    inspector = Inspector(&title); // error[E0597]: `title` does not live long enough
    // Locals declared in one `let` are dropped in reverse order: `title` first, then `inspector`,
    // whose Drop would read a freed String.
}

Without the Drop impl, the same code compiles: the compiler knows dropping a plain struct with a reference does not read through it.

Vec<T> and Box<T> need to say "my destructor drops T but does not otherwise access borrowed data". The standard library does that with the #[may_dangle] attribute on its Drop impls. #[may_dangle] requires the dropck_eyepatch feature and is therefore nightly-only and unsafe; on stable you cannot write it, and custom collections that wrap Vec get the right behaviour by containing one (and using PhantomData<T> to declare ownership when they store raw pointers).

Controlling drops: drop, forget, ManuallyDrop, take and replace

Tool Use

std::mem::drop(x)

drop now, before the end of the scope (release a lock early, free memory)

std::mem::forget(x)

never run x's destructor; leaks its resources (safe, but rarely right)

std::mem::ManuallyDrop<T>

a wrapper whose contents are not dropped automatically; call ManuallyDrop::drop (unsafe) or into_inner yourself. Used to control drop order and in FFI

std::mem::take(&mut x)

move the value out and leave Default::default() behind

std::mem::replace(&mut x, new)

move the value out and leave new behind

Option::take()

move out of an Option, leaving None

use std::mem;

#[derive(Debug, Default)]
enum Loan {
    #[default]
    None,
    Active { member: String },
    Returned { member: String },
}

impl Loan {
    // Change state through &mut self without cloning the String: take it out, build the new state.
    fn give_back(&mut self) {
        *self = match mem::take(self) {
            Loan::Active { member } => Loan::Returned { member },
            other => other,
        };
    }
}

fn main() {
    let mut loan = Loan::Active { member: "alice".into() };
    loan.give_back();
    println!("{loan:?}"); // Returned { member: "alice" }

    let mut queue = vec![1, 2, 3];
    let drained = mem::replace(&mut queue, Vec::with_capacity(8));
    println!("{drained:?} {queue:?}");
}

Panic safety, double panics and catch_unwind

When a panic unwinds, destructors run on the way up. Two consequences:

  • Panic safety (exception safety): code that temporarily breaks an invariant, then calls something that may panic, can leave a data structure in an inconsistent state that a destructor or a later user observes. Safe Rust cannot cause undefined behaviour this way, but unsafe code must be careful (the Rustonomicon’s "Exception Safety" chapter shows the techniques). Mutex marks itself poisoned when a thread panics while holding the lock, so other threads learn the data may be inconsistent.

  • Double panics: if a destructor panics while the thread is already unwinding from a panic, the process aborts. Destructors should therefore never panic.

std::panic::catch_unwind runs a closure and turns a panic into an Err. It requires the closure to be UnwindSafe — a marker that warns when captured &mut or interior-mutable data might be observed in a broken state; wrap with AssertUnwindSafe when you have checked it is fine. It is meant for FFI boundaries and thread-pool or server infrastructure, not for ordinary error handling, and it does nothing when panics abort (panic = "abort").

use std::panic::{self, AssertUnwindSafe};

fn main() {
    panic::set_hook(Box::new(|info| eprintln!("custom hook: {info}"))); // replace the default message
    let mut processed = 0;
    let result = panic::catch_unwind(AssertUnwindSafe(|| {
        processed += 1;
        if processed == 1 {
            panic!("row 7 is corrupt");
        }
    }));
    let _ = panic::take_hook(); // restore the default hook
    println!("caught: {} after {processed} row(s)", result.is_err());
}

Leaking

Leaking — never running a destructor — is memory-safe in Rust, and therefore allowed in safe code: mem::forget, Box::leak, Rc cycles. A consequence for unsafe code is that you may not rely on a destructor running for soundness. That is why std::thread::scope (which guarantees joins) is a closure-based API, and why the old thread::scoped API that relied on a guard’s destructor was removed before Rust 1.0.

Self-referential types

A struct cannot safely contain a reference to one of its own fields, because moving the struct would invalidate the reference. Async state machines need exactly that, which is the problem Pin solves; see Pin & Unpin.

Where the books are outdated

Azam (Advanced Rust, chapter 9) covers the drop check, #[may_dangle], drop order and partial moves, self-referential structs, mem::forget, ManuallyDrop, panic safety, double panics and catch_unwind. Note that #[may_dangle] is a nightly-only attribute (feature dropck_eyepatch) and cannot be used on stable Rust, and that the 2024 edition changed the drop order of temporaries in if let conditions and tail expressions.