The Infinite Recursion Trap: Why Rust’s Drop Trait Refuses to Take Ownership

Drop is the trait that makes cleanup automatic: when a value dies, its drop method runs, and the file is closed, the lock is released, the memory goes back. The first post in this series covers the everyday side: values die at the end of their scope in reverse order, let _ = make_value() drops the fresh value at once, drop(value) ends a value early, and you cannot call .drop() yourself. This post is about one line of that trait and everything that follows from it:

// fragment: the trait as declared in the standard library
pub trait Drop {
    fn drop(&mut self);
}

Why &mut self? Everywhere else, “this is the last thing that happens to the value” is spelled self: into_iter(self), JoinHandle::join(self), Option::unwrap(self). The destructor is the most final thing of all, and yet it only borrows. The reason is a trap that self would spring, and understanding it shows you exactly what the compiler does at the moment a value dies.

Checked in September 2026 against Rust 1.98.1, edition 2024. Every code block compiles and runs as shown, except the ones whose first line starts with ✗: those are meant to fail, so you can see the exact error, or in one case the exact crash.

The running example is the café till from the first post: a cash Drawer that must be locked when a shift ends, and a barista thread that must be waited for before the shop closes.

What happens when a value dies

The Reference defines it in two steps. Running the destructor of a type T means: first, if T implements Drop, call <T as Drop>::drop; then, recursively, run the destructors of all of its fields, in declaration order. Your code runs first, the fields are cleaned up after it, and nothing you write inside drop can skip the second step.

struct Noisy(&'static str);

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

#[allow(dead_code)]
struct Order {
    cup: Noisy,
    lid: Noisy,
}

impl Drop for Order {
    fn drop(&mut self) {
        println!("closing the order: your code runs first");
    }
}

fn main() {
    let _order = Order { cup: Noisy("cup"), lid: Noisy("lid") };
    println!("end of main");
}
// Output:
// end of main
// closing the order: your code runs first
// dropped cup
// dropped lid

Both steps together are generated by the compiler for every type, whether or not it implements Drop; people call the generated code drop glue. A String has no Drop implementation of its own: its glue drops the Vec<u8> inside, and that Vec’s destructor returns the heap buffer. Keep the two steps in mind; the whole design of the signature follows from them.

The forbidden signature

Suppose you wrote what intuition suggests:

// ✗ Does not compile. Error: E0053 method `drop` has an incompatible type for trait
struct Drawer;

impl Drop for Drawer {
    fn drop(self) {
        println!("locking the drawer");
    }
}

fn main() {
    let _d = Drawer;
}

The compiler answers with the note expected signature fn(&mut Drawer), found signature fn(Drawer). Now imagine it did not, and follow one Drawer to the end of main:

  1. _d goes out of scope, so the compiler runs its destructor: step one, call drop.
  2. drop(self) takes the value by value, so the drawer moves into the parameter self.
  3. The body runs and prints its message.
  4. The body ends, and self goes out of scope. It is an initialised Drawer, and initialised values that go out of scope are destroyed. Step one: call drop.
  5. Go to 2.

Each round pushes a new frame on the stack, and the program dies of a stack overflow. The trait signature is the only thing standing between you and that loop. Ask on the Rust forums why drop cannot take self, and the answers from long-time contributors are short: with fn drop(self) “you somehow need to not Drop the self the second time” (matklad, 2018), and “If self is passed by value, then Drop will be called again at the end of the function. Or do you expect every single impl to correctly forget(self)? Because that’s a huge footgun. The fact that Drop::drop() takes a reference is no accident.” (H2CO3, 2023). Every fix needs a special rule for this one function: the compiler would have to remember that the value inside drop must not be destroyed again, and it would have to decide who drops the fields, and when. &mut self needs only one rule, and it is a simple one: you may not call drop yourself (E0040, shown in the first post), so the compiler’s call is the only call.

Why a borrow is exactly enough

Think about what a destructor needs to do: flush a buffer, release a lock, mark a record as closed, decrement a counter. All of that is a change to the value’s own state or to something it points at, and a &mut is the most that any change needs. What a destructor does not need is to destroy the container, because the compiler’s second step and the owner of the memory do that: the fields are dropped by the glue, and the stack frame, the Box or the Vec that held the bytes gives them back.

struct Drawer {
    cash: u32,
    locked: bool,
}

impl Drop for Drawer {
    fn drop(&mut self) {
        self.locked = true;                                          // a final change, through `&mut self`
        println!("drawer locked with {} inside: {}", self.cash, self.locked);
    }
}

fn main() {
    let drawer = Drawer { cash: 120, locked: false };              // note: not `mut`
    println!("open for business: {}", !drawer.locked);
}
// Output:
// open for business: true
// drawer locked with 120 inside: true

When drop returns, the reference &mut self simply ends; dropping a reference does nothing. Then the compiler drops cash and locked (an integer and a boolean: nothing to do) and main’s frame is popped. One value, destroyed exactly once.

“But the binding was not mut”

The drawer above was bound with a plain let, and yet drop changed it through &mut self. Is that an exception to Rust’s rules? No. mut on a binding governs what you may do through that name: assign to it, or take a &mut of it. The destructor is not something you do through the name. It is code the compiler generates, run on the value’s memory after the name’s last use, at a point where the borrow checker has already proved that no reference to the value is alive. Exclusive access to something nobody else can see is precisely what the many-readers-or-one-writer rule allows, so the rule is upheld, not suspended.

The first post shows the same fact from the other side: you can move a value out of an immutable name into a mutable one and change it, because the value was never immutable, only its first name was. And std::mem::drop shows it a third way. The standard library’s documentation says of it: “This function is not magic; it is literally defined as pub fn drop<T>(_x: T) {}”. The parameter _x is not mut either, and the value still dies through Drop::drop(&mut _x) when the function returns. You can write the same function yourself:

struct Drawer;

impl Drop for Drawer {
    fn drop(&mut self) {
        println!("locking the drawer");
    }
}

fn lock_now<T>(_x: T) {}       // the same shape as std::mem::drop: take ownership, do nothing

fn main() {
    let d = Drawer;            // not `mut`
    lock_now(d);               // "locking the drawer" prints here: `_x` dies when lock_now returns
    println!("after lock_now");
}
// Output:
// locking the drawer
// after lock_now

Its opposite, std::mem::forget, is just as small, pub const fn forget<T>(t: T) { let _ = ManuallyDrop::new(t); }: it wraps the value in ManuallyDrop, the type whose destructor does nothing, and lets that wrapper die. ManuallyDrop comes back below.

Stack or heap: drop does not know

A question that comes up as soon as people see &mut self: inside drop, does the code know whether the value lives on the stack or on the heap? It does not, and it does not need to. self is an address, and the same instructions run whatever memory it points into. Where the bytes live is the owner’s business: a Box frees its heap block after the contents’ destructor has run, and a stack frame is simply popped.

struct Drawer(&'static str);

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

fn main() {
    let on_stack = Drawer("stack drawer");
    let on_heap = Box::new(Drawer("heap drawer"));   // the Drawer is on the heap; `on_heap` is the Box that owns it
    println!("{} and {}", on_stack.0, on_heap.0);
}
// Output:
// stack drawer and heap drawer
// locking the heap drawer
// locking the stack drawer

on_heap was declared last, so it dies first: Box’s destructor runs the Drawer’s drop, then frees the heap block. Then on_stack dies with the frame. Same drop body, two homes.

Moving a field out of drop

Because drop only has &mut self, it cannot move a field out. That bites the moment cleanup needs a consuming call, and the classic one is waiting for a thread: JoinHandle::join takes self.

// ✗ Does not compile. Error: E0507 cannot move out of `self.handle` which is behind a mutable reference
use std::thread;

struct Barista {
    handle: thread::JoinHandle<u32>,
}

impl Drop for Barista {
    fn drop(&mut self) {
        if let Ok(cups) = self.handle.join() {    // join(self) would move the handle out of `*self`
            println!("barista cleaned {cups} cups");
        }
    }
}

fn main() {
    let _b = Barista { handle: thread::spawn(|| 12) };
}

The everyday fix is to store the field as an Option and take it: Option::take swaps None into the field and hands you what was there, so the struct is still whole and the compiler is satisfied.

use std::thread;

struct Barista {
    handle: Option<thread::JoinHandle<u32>>,
}

impl Drop for Barista {
    fn drop(&mut self) {
        if let Some(handle) = self.handle.take() {          // move the handle out, leave None behind
            if let Ok(cups) = handle.join() {               // never panic inside drop: see "A panic inside drop" below
                println!("barista cleaned {cups} cups and went home");
            }
        }
    }
}

fn main() {
    let _b = Barista { handle: Some(thread::spawn(|| 12)) };   // the thread counts the cups it cleaned
    println!("closing time");
}
// Output:
// closing time
// barista cleaned 12 cups and went home

For a field whose type has a cheap “empty” value, std::mem::take does the same job without the Option: it needs T: Default and leaves the default behind. std::mem::replace lets you choose the replacement yourself.

struct Receipt {
    lines: Vec<String>,
}

impl Drop for Receipt {
    fn drop(&mut self) {
        let lines = std::mem::take(&mut self.lines);   // move the Vec out; an empty Vec stays behind
        println!("printing {} line(s): {}", lines.len(), lines.join(" | "));
    }
}

fn main() {
    let _r = Receipt { lines: vec![String::from("latte 3.50"), String::from("scone 2.00")] };
    println!("customer paid");
}
// Output:
// customer paid
// printing 2 line(s): latte 3.50 | scone 2.00

This is not only a rule about drop. A type that implements Drop refuses to let anyone move a field out of it, anywhere in the program, because after the move the destructor would run on a half-empty value:

// ✗ Does not compile. Error: E0509 cannot move out of type `Receipt`, which implements the `Drop` trait
struct Receipt {
    lines: Vec<String>,
}

impl Drop for Receipt {
    fn drop(&mut self) {
        println!("printing");
    }
}

fn main() {
    let receipt = Receipt { lines: vec![String::from("latte 3.50")] };
    let lines = receipt.lines;   // a partial move out of a Drop type
    println!("{lines:?}");
}

Without the impl Drop, that partial move is fine; the first post’s Part Eight shows it. Option::take, mem::take and mem::replace are how you move things out of a Drop type, inside drop or outside it.

The expert’s version. An Option costs a check and, for some types, extra bytes. ManuallyDrop<T> wraps a value so that the compiler’s glue leaves it alone, and ManuallyDrop::take moves the value out through a &mut. It is an unsafe fn, because after the take the wrapper holds a value that must never be used or dropped again; inside drop, where nothing of yours runs afterwards, that promise is easy to keep:

use std::mem::ManuallyDrop;
use std::thread;

struct Barista {
    handle: ManuallyDrop<thread::JoinHandle<u32>>,
}

impl Drop for Barista {
    fn drop(&mut self) {
        // SAFETY: `handle` is taken exactly once, here, and nothing reads it afterwards;
        // ManuallyDrop guarantees the compiler will not drop it a second time.
        let handle = unsafe { ManuallyDrop::take(&mut self.handle) };
        if let Ok(cups) = handle.join() {                   // never panic inside drop
            println!("barista cleaned {cups} cups and went home");
        }
    }
}

fn main() {
    let _b = Barista { handle: ManuallyDrop::new(thread::spawn(|| 12)) };
    println!("closing time");
}
// Output:
// closing time
// barista cleaned 12 cups and went home

Reach for it when a profiler says the Option matters; otherwise Option::take says what you mean with no unsafe.

A panic inside drop

Destructors also run while a panic is unwinding: the stack is being torn down and every live value on the way is dropped. If one of those destructors panics too, Rust cannot unwind two panics at once, so it stops the process:

// ✗ Compiles, but aborts at run time: panic in a destructor during cleanup
struct Drawer;

impl Drop for Drawer {
    fn drop(&mut self) {
        panic!("the drawer is jammed");   // a second panic, while the first is still unwinding
    }
}

fn main() {
    let _d = Drawer;
    panic!("the printer is on fire");    // unwinding starts here and reaches `_d`
}

The program prints both panic messages (the second one followed by a full stack backtrace, which Rust prints for a panic during a panic even without RUST_BACKTRACE), then a third panic message, panic in a destructor during cleanup, then thread caused non-unwinding panic. aborting., and it is killed by SIGABRT (signal 6) rather than exiting normally; a Unix shell reports that as exit status 134, that is 128 + 6. A panic in drop while nothing is unwinding is an ordinary panic, and catch_unwind can catch it:

struct Drawer;

impl Drop for Drawer {
    fn drop(&mut self) {
        panic!("the drawer is jammed");
    }
}

fn main() {
    let result = std::panic::catch_unwind(|| {
        let _d = Drawer;          // nothing is unwinding yet, so this is an ordinary panic
    });
    println!("caught: {}", result.is_err());   // the panic message itself went to stderr
}
// Output:
// caught: true

The abort case is why the advice is simple: a destructor should not panic. Inside drop, prefer if let Ok(..) = handle.join() or let _ = handle.join(); to .expect(...), and log instead of panicking; the two barista examples above do exactly that.

When drop does not run at all

Rust guarantees that a value is dropped at most once, not that it is dropped at all. Four ways to skip it, all in safe code:

use std::cell::RefCell;
use std::rc::Rc;

struct Noisy(&'static str);

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

#[allow(dead_code)]
struct Node {
    tag: Noisy,
    next: RefCell<Option<Rc<Node>>>,
}

fn main() {
    let forgotten = Noisy("forgotten");
    std::mem::forget(forgotten);                                   // 1. forget: the destructor is skipped on purpose

    let manual = std::mem::ManuallyDrop::new(Noisy("manual"));     // 2. ManuallyDrop: the same, as a type
    println!("{} is wrapped", manual.0);

    let a = Rc::new(Node { tag: Noisy("cycle a"), next: RefCell::new(None) });
    let b = Rc::new(Node { tag: Noisy("cycle b"), next: RefCell::new(Some(Rc::clone(&a))) });
    *a.next.borrow_mut() = Some(Rc::clone(&b));                    // 3. a reference cycle: neither count reaches zero

    let _kept = Noisy("kept until exit");
    println!("calling process::exit");
    std::process::exit(0);                                         // 4. exit: no destructor runs after this line
}
// Output:
// manual is wrapped
// calling process::exit

The Reference says this out loud: apart from the places where destructors are guaranteed to run, “types may not safely rely on a destructor being run for soundness”. A lock guard that is forgotten stays locked; a Box that is forgotten is a leak, not a crash. If something must happen at the end, do it explicitly before main returns, and treat drop as the safety net.

Two more rules the compiler enforces

Drop and Copy exclude each other. A Copy type is duplicated by copying its bytes, and two copies would mean two destructor runs for one resource, so #[derive(Copy)] on a type with a Drop impl is refused:

// ✗ Does not compile. Error: E0184 the trait `Copy` cannot be implemented for this type; the type has a destructor
#[derive(Clone, Copy)]
struct Drawer;

impl Drop for Drawer {
    fn drop(&mut self) {}
}

fn main() {}

A Drop impl must cover the whole type. You cannot write a destructor for Wrapper<String> only; the glue for Wrapper<T> is one thing, decided for every T:

// ✗ Does not compile. Error: E0366 `Drop` impls cannot be specialized
struct Wrapper<T>(T);

impl Drop for Wrapper<String> {
    fn drop(&mut self) {
        println!("dropping a string wrapper");
    }
}

fn main() {}

Conclusion

fn drop(&mut self) is not a quirk; it is the smallest signature that works. By value, the parameter would itself need destroying, and the destructor would call itself until the stack ran out. By exclusive reference, your code makes its final changes, the reference ends, and the compiler destroys the fields and returns the memory, exactly once. The consequences follow: you cannot call drop yourself (E0040), you cannot move a field out of a Drop type (E0509, or E0507 inside drop) without Option::take, mem::take or ManuallyDrop, a destructor must never panic during unwinding, and nothing guarantees that a destructor runs at all. It is the kind of design decision that looks obvious in hindsight, because someone thought hard about what could go wrong.

Sources