Panic and Unwinding
When a thread panics (panic!(), unwrap(), out-of-bounds index), the Rust runtime by default initiates stack unwinding: it walks backward up the active call stack frame by frame, running the Drop destructor of every initialized local variable in each frame in reverse declaration order.
Unwind vs. Abort Modes
| Panic Strategy | Cargo.toml Setting |
Behavior on panic!() |
Destructors (Drop) Executed? |
|---|---|---|---|
| Unwind (default) | panic = "unwind" |
Walks up the call stack frame by frame; can be caught at thread boundary or catch_unwind |
Yes, for all live variables in every unwound stack frame |
| Abort | panic = "abort" |
Immediately terminates the process via OS abort signal (SIGABRT / __fastfail) |
No; OS reclaims process memory and closes file descriptors directly |
Stack Unwinding on Panic:
Frame 3: inner() ---> panic!("boom")
| - drops initialized locals in inner()
v
Frame 2: middle() ---> - drops initialized locals in middle()
|
v
Frame 1: main() ---> - drops initialized locals in main() (or stops at catch_unwind)
use std::panic;
struct StepGuard(&'static str);
impl Drop for StepGuard {
fn drop(&mut self) {
println!(" [unwind cleanup] dropping {}", self.0);
}
}
fn failing_step() {
let _g2 = StepGuard("step 2 resource");
panic!("simulated failure in failing_step");
}
fn outer_workflow() {
let _g1 = StepGuard("step 1 resource");
failing_step();
}
fn main() {
let result = panic::catch_unwind(|| {
outer_workflow();
});
println!("caught panic: {}", result.is_err());
}
[unwind cleanup] dropping step 2 resource
[unwind cleanup] dropping step 1 resource
caught panic: true
The Double-Panic Abort Rule
If a Drop::drop implementation itself panics while the thread is already unwinding from an earlier panic, the Rust runtime immediately aborts the process (thread panicked while processing panic. aborting.). Unwinding cannot track two simultaneous active panics on the same thread stack.
Consequently, destructors (Drop::drop) must never panic. You can check std::thread::panicking() inside Drop::drop to detect whether the destructor is running as part of a normal scope exit (false) or a panic unwind (true):
use std::panic;
use std::thread;
struct Transaction {
committed: bool,
}
impl Transaction {
fn begin() -> Self {
Self { committed: false }
}
fn commit(mut self) {
println!("transaction committed");
self.committed = true;
}
}
impl Drop for Transaction {
fn drop(&mut self) {
if !self.committed {
if thread::panicking() {
println!("rolling back uncommitted transaction during panic unwind");
} else {
println!("rolling back uncommitted transaction on normal scope exit");
}
}
}
}
fn main() {
println!("--- 1. Committed transaction ---");
{
let tx = Transaction::begin();
tx.commit();
}
println!("--- 2. Dropped without commit ---");
{
let _tx = Transaction::begin();
}
println!("--- 3. Dropped during panic unwind ---");
let _ = panic::catch_unwind(|| {
let _tx = Transaction::begin();
panic!("query failed");
});
}
--- 1. Committed transaction ---
transaction committed
--- 2. Dropped without commit ---
rolling back uncommitted transaction on normal scope exit
--- 3. Dropped during panic unwind ---
rolling back uncommitted transaction during panic unwind
Exception Safety (Unwind Safety) in Unsafe Code
When writing unsafe code that temporarily violates a type's invariant (for example, moving an element out of a buffer before shifting the remaining elements), if a user-provided closure or Clone::clone() panics mid-operation, the enclosing struct's Drop implementation will still run during unwinding. Unsafe code must ensure the struct remains in a valid, safely droppable state before calling any function that might unwind.