panic!
The panic! macro immediately halts normal execution on the current thread when the program encounters an unrecoverable bug or violated invariant. It prints an error message and source location to standard error (stderr), cleans up the thread's call stack by running destructors (unwinding), and terminates the thread.
Invoking panic! Explicitly and Implicitly
A panic can be triggered in two ways:
- Explicitly: By calling
panic!("...")(which accepts format strings and arguments just likeprintln!), or related macros such asassert!,assert_eq!,unreachable!,todo!, or.unwrap()/.expect(). - Implicitly by runtime safety checks: By performing an illegal operation such as indexing past the end of a slice (
slice[99]), dividing an integer by zero (x / 0), or overflowing an integer in debug mode.
fn set_concurrency_limit(threads: usize) {
if threads == 0 {
panic!("concurrency limit must be >= 1, but got {threads}");
}
println!("Configured worker pool with {threads} thread(s).");
}
fn main() {
set_concurrency_limit(4);
}
Configured worker pool with 4 thread(s).
If set_concurrency_limit(0) is called, the program halts and prints to stderr:
thread 'main' panicked at src/main.rs:3:9:
concurrency limit must be >= 1, but got 0
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Inspecting Call Stack Backtraces (RUST_BACKTRACE)
When a panic occurs deep inside nested function calls, setting the RUST_BACKTRACE environment variable instructs the Rust runtime to print the full stack trace of active function calls leading up to the panic:
| Environment Variable Setting | What Is Printed on Panic |
|---|---|
| Unset (default) | Only the panic message, file path, line number, and column |
RUST_BACKTRACE=1 |
Short backtrace showing your functions and relevant standard library calls |
RUST_BACKTRACE=full |
Complete backtrace including all internal runtime startup frames and memory addresses |
RUST_BACKTRACE=1 cargo run
thread 'main' panicked at src/main.rs:3:9:
concurrency limit must be >= 1, but got 0
stack backtrace:
0: rust_begin_unwind
1: core::panicking::panic_fmt
2: my_app::set_concurrency_limit
at ./src/main.rs:3:9
3: my_app::main
at ./src/main.rs:8:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
Built-In Panic Helper Macros
In addition to panic!, the standard library prelude includes specialized macros that panic with clear, standardized messages:
| Macro | Purpose | When It Panics |
|---|---|---|
panic!("fmt", ...) |
Unconditional panic with custom message | Always when reached |
assert!(cond, "fmt", ...) |
Verifies a boolean invariant in both Debug and Release builds | When cond is false |
assert_eq!(left, right) |
Verifies left == right and prints both values on failure |
When left != right |
assert_ne!(left, right) |
Verifies left != right and prints both values on failure |
When left == right |
unreachable!("reason") |
Marks a branch that logical invariants guarantee can never be reached | Always if reached (internal error: entered unreachable code) |
todo!("task") |
Placeholder for unfinished code that satisfies any return type (!) |
Always if executed (not yet implemented) |
unimplemented!() |
Marks an intentionally unsupported trait method | Always if executed (not implemented) |
fn format_quarter(quarter: u8) -> &'static str {
assert!((1..=4).contains(&quarter), "quarter must be 1..=4, got {quarter}");
match quarter {
1 => "Q1 (Jan–Mar)",
2 => "Q2 (Apr–Jun)",
3 => "Q3 (Jul–Sep)",
4 => "Q4 (Oct–Dec)",
_ => unreachable!("assert above already verified that quarter is in 1..=4"),
}
}
fn main() {
println!("Quarter 3: {}", format_quarter(3));
}
Quarter 3: Q3 (Jul–Sep)
Because panic!, unreachable!, and todo! all have the never type (!), they can stand in for any return type—allowing your code to compile and type-check while you are still implementing other branches with todo!().
What Happens During a Panic: Unwinding and Thread Isolation
When panic! executes (under the default panic = "unwind" strategy):
1. `panic!` is called in `inner_fn()`
│
v
2. Drops all initialized local variables in `inner_fn()` (LIFO order)
│
v
3. Walks up to `caller_fn()` and drops all local variables in `caller_fn()`
│
├── If on the `main` thread: exits process with code 101 after unwinding `main()`
└── If on a spawned worker thread (`thread::spawn`): terminates ONLY that worker
thread and returns `Err(Any)` when the parent calls `handle.join()`!
struct CleanupGuard(&'static str);
impl Drop for CleanupGuard {
fn drop(&mut self) {
println!(" [Drop during unwind] Safely cleaned up {}", self.0);
}
}
fn main() {
// Spawn a worker thread that panics:
let handle = std::thread::spawn(|| {
let _guard = CleanupGuard("worker thread resource");
panic!("worker encountered an unrecoverable bug");
});
// The main thread is NOT crashed by the worker thread's panic;
// `handle.join()` catches the thread termination as an `Err`:
let outcome = handle.join();
println!("Main thread survived! Worker join status is_err = {}", outcome.is_err());
}
[Drop during unwind] Safely cleaned up worker thread resource
Main thread survived! Worker join status is_err = true