Error Handling

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:

  1. Explicitly: By calling panic!("...") (which accepts format strings and arguments just like println!), or related macros such as assert!, assert_eq!, unreachable!, todo!, or .unwrap() / .expect().
  2. 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