#293 Jul 31, 2026

293. thread::panicking — A Second Panic in Drop Doesn't Unwind, It Aborts

Your Drop impl asserts a cleanup invariant. A test fails, unwinding starts, your destructor runs — and its assert fails too. Two panics at once: the process aborts and the original error message is gone.

Rust can’t unwind twice at the same time. If a destructor panics while the thread is already unwinding from an earlier panic, the runtime gives up and calls abort() — no unwinding, no catch_unwind rescue (bite 291), just a dead process that hides the failure you actually cared about.

The problem:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
struct Guard {
    committed: bool,
}

impl Drop for Guard {
    fn drop(&mut self) {
        // If a panic is already unwinding past this
        // guard, this second panic aborts the process.
        assert!(self.committed, "guard dropped uncommitted");
    }
}

Any panic between creating the guard and committing it now aborts instead of unwinding — your test harness never even gets to print the real failure.

std::thread::panicking() tells you whether the current thread is mid-unwind, so the destructor can stand down:

1
2
3
4
5
6
7
8
impl Drop for Guard {
    fn drop(&mut self) {
        if std::thread::panicking() {
            return; // already unwinding — don't make it worse
        }
        assert!(self.committed, "guard dropped uncommitted");
    }
}

The original panic unwinds cleanly, catch_unwind and your panic hook (bite 292) see it as usual, and the guard still enforces its invariant on every normal exit path.

Rule of thumb: any Drop that can panic — asserts, unwrap, flushing a writer — should either swallow the error or check thread::panicking() first.

← Previous 292. panic::set_hook — You Caught the Panic, but It Still Screamed to stderr Next → 294. AssertUnwindSafe — catch_unwind Won't Touch Your &mut Until You Sign the Waiver