Unwinding and Aborting
Understand how Rust handles panics through stack unwinding or immediate abort, the trade-offs between the two strategies, and how to configure your project to use the one that fits your needs.
When a panic! occurs, the program must stop execution. Rust offers two ways to do that: it can walk back up the call stack, cleaning up each function’s local data as it goes, or it can terminate immediately without any cleanup. The first is called unwinding and is the default. The second is called aborting and must be opted into. This page explains both mechanisms, why you might choose one over the other, and how to set the behavior in your own projects.
How Unwinding Works
By default, panic = "unwind" is set for all build profiles. When a thread panics:
- Rust calls the panic hook, which by default prints the panic message and a backtrace if
RUST_BACKTRACEis set. - The runtime begins to unwind the stack — frame by frame, from the point of the panic up through the call chain. For each stack frame, the destructors (
Dropimplementations) of local variables are executed. - If the panic is not caught by
std::panic::catch_unwind, the unwinding continues until it reaches the top of the thread’s stack, at which point the thread terminates. - If the panicking thread is the main thread, the process exits with a non-zero exit code (101).
Unwinding guarantees that resources held by local variables are released — files get closed, network sockets are dropped, locks are unlocked. This is the same machinery that runs when a scope ends normally; a panic just forces an early, orderly exit from every scope along the way.
Double Panic:
If a destructor or the panic hook itself panics during unwinding, the program has entered an unrecoverable double‑panic state. Rust immediately aborts the process to avoid undefined behavior. This safety net exists only in the unwind strategy — if you compile with panic = "abort", the concept of a double panic never arises because the first panic terminates the process before any destructors run.
How Aborting Works
When the panic strategy is set to "abort", the sequence changes dramatically. After the panic hook is called, Rust calls std::process::abort() (or its platform equivalent). The operating system terminates the process at once.
No destructors run. No stack unwinding happens. File handles, heap memory, and other resources are reclaimed by the OS, but any application‑level cleanup — flushing a buffer, deleting a temporary file, sending a graceful shutdown message — does not occur. The program stops dead.
This behavior is often the right choice when the program has reached a state so broken that running further code (including destructors) could be dangerous or when you simply don’t need the cleanup overhead.
Why Abort Exists
The unwind strategy carries a cost, even when no panic happens. The compiler must generate unwinding metadata (landing pads, exception tables) that increases binary size and can slightly affect optimizations. On resource‑constrained targets, this extra code and data is unacceptable. Abort removes all of that, producing smaller, leaner executables.
Beyond size, abort is mandatory in many no_std and embedded environments where unwinding support is simply not present. Kernels, firmware, and some system libraries cannot afford the runtime machinery needed to walk the stack. For those worlds, panic = "abort" is not a preference — it’s a requirement.
Another practical concern is catch_unwind. Rust’s mechanism for catching panics only works when the code is compiled with panic = "unwind". If you ship a library that catches panics and you compile it with abort, the library will still compile, but any panic inside a catch_unwind block will abort the entire process instead of being caught. That surprises developers who assume catch_unwind always works. If your library relies on catching panics, you must communicate that it needs the unwind strategy.
catch_unwind and abort are incompatible:
Compiling with panic = "abort" makes every std::panic::catch_unwind call useless. The panic handler will still run, but the process will terminate before control returns to the catching code. Do not use catch_unwind in a library without documenting that it requires panic = "unwind".
Configuring the Panic Strategy
You set the panic behavior per build profile in Cargo.toml. The relevant key is panic inside a [profile.*] section. It accepts either "unwind" or "abort".
For a typical release‑mode binary that trades size for cleanup safety:
[profile.release]
panic = "abort"
If you want to keep unwinding in debug builds (so tests can catch panics) but abort in the final release artifact, leave the [profile.dev] and [profile.test] sections unchanged and only add panic = "abort" under [profile.release].
You can achieve the same effect from the command line by passing -C panic=abort to rustc, but Cargo.toml is the recommended approach because it keeps the setting version‑controlled and reproducible.
Profile Scope:
The configuration applies only to the profile you place it under. If you set panic = "abort" only in [profile.release], your cargo test and cargo run (dev profile) will still use unwinding. This split is common: you want the ability to catch panics during development and testing, but you ship a small, abort‑on‑panic binary.
Checking the Active Strategy in Code
Rust exposes the chosen panic strategy at compile time through a cfg predicate. You can query it with cfg! or use #[cfg] for conditional compilation.
fn main() {
if cfg!(panic = "unwind") {
println!("Panic strategy: unwind");
} else {
println!("Panic strategy: abort");
}
}
A library can use this to adapt its behavior. For example, a function that normally calls catch_unwind might instead panic‑and‑abort (or return an error) when the strategy is abort.
fn run_safely<F: FnOnce()>(f: F) {
#[cfg(panic = "unwind")]
{
if let Err(_) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)) {
eprintln!("caught a panic");
}
}
#[cfg(not(panic = "unwind"))]
{
f();
}
}
This kind of conditional compilation is rare in application code but valuable in low‑level libraries that need to work under both strategies.
The Panic Hook Runs in Both Strategies
Regardless of whether you unwind or abort, the panic hook is always invoked first. The hook is set by std::panic::set_hook and, by default, prints the panic message and file location. You can override it to log crash information, write a report to disk, or trigger an external alert.
Because the hook runs before the final decision to unwind or abort, you can use it as a last‑chance crash reporter even in abort mode. However, keep the hook minimal: a hook that panics while aborting will still cause the process to abort immediately (a double panic in unwind mode, or a nested panic in abort mode), and any further logging may be lost.
Built‑in Crash Visibility:
If your program is compiled with panic = "abort", the panic message and file location still appear in stderr because the default hook outputs them. This means you retain basic diagnostic information without the extra binary size of unwinding support.
Choosing Between Unwind and Abort
The decision boils down to a single trade‑off: cleanup guarantees versus binary size and simplicity.
- Stick with
unwind(the default) when your application or library needs to catch panics, when orderly resource cleanup matters even during a crash, or when you are writing general‑purpose code that will run in unknown environments. - Switch to
abortwhen you are building a release binary and want the smallest possible artifact, when you are in an embedded orno_stdcontext where unwinding is unavailable, or when you have decided that any panic represents a fatal, unrecoverable state and that running destructors is either unnecessary or dangerous.
Many Rust projects keep unwind during development (to benefit from catch_unwind in tests) and use abort only in the release profile. That pattern often delivers the best of both worlds.
Relationship with std::process::abort
It’s worth noting that std::process::abort() is a separate function from the panic strategy. Calling std::process::abort() directly terminates the process without invoking the panic hook, without running any destructors, and without any Rust cleanup. It’s a raw, immediate exit.
When you configure panic = "abort" and a panic occurs, Rust calls the panic hook first and then aborts. So panicking with abort strategy still gives you a chance to log, whereas a direct call to std::process::abort does not.
Summary
The unwinding and aborting strategies are two sides of the same coin: both terminate the program after an unrecoverable error, but they differ in what happens between the error and the exit. Unwinding cleans up, aborting cuts to the end.
Which one is right depends on your context. Understanding both gives you control over the footprint, behavior, and safety characteristics of your Rust programs.