12.1panic! and unrecoverable errors

Last updated June 13, 2026

Rust splits failure into two kinds, and gives each its own tool. Some failures are recoverable: a file isn't there, the user typed letters where you wanted a number, a network request timed out. The program can sensibly respond, and chapter 12's main subject, Result, is how. Other failures are unrecoverable: a bug has put the program in a state that should be impossible, and continuing would only make things worse. For those, Rust panics. This lesson is about panicking, which you've witnessed since chapter 3 and now get to understand and cause on purpose.

What a panic actually does

You've seen panics for ten chapters: integer overflow in debug (lesson 4.4), an out-of-range slice (lesson 9.6), unwrap on a None (lesson 11.3). All of those call the same underlying mechanism, and you can call it directly with the panic! macro:

fn main() {
    println!("about to panic");
    panic!("something went badly wrong");
    println!("this never runs");
}
about to panic

thread 'main' panicked at src/main.rs:3:5:
something went badly wrong
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

When a panic happens, the program prints your message, the file and line where it occurred, and a note about backtraces, then it unwinds: it walks back up the call stack (lesson 8.2), dropping every value along the way so cleanup still runs (lesson 8.3 holds even in failure), and then the thread stops. The line after the panic! never executes; a panic does not return, it ends the path it's on. For a single-threaded program, a panic in main ends the whole program with a failure exit code (lesson 7.8).

This is deliberately abrupt. A panic means "an assumption I built the program on turned out to be false," and Rust's stance is that the safest response to a violated assumption is to stop cleanly rather than stumble forward through corrupted state. That's the opposite of the silent-wrong-answer failure mode lesson 1.7 said Rust works to eliminate. A panic is loud on purpose.

Reading a backtrace

The panic message tells you where the panic fired, but when the real cause is several function calls up, you want the whole chain. Setting the RUST_BACKTRACE environment variable turns it on:

RUST_BACKTRACE=1 cargo run
thread 'main' panicked at src/main.rs:3:5:
something went badly wrong
note: run with `RUST_BACKTRACE=full` for a verbose backtrace
stack backtrace:
   0: rust_begin_unwind
   1: core::panicking::panic_fmt
   2: my_program::cause_trouble
   3: my_program::main
   ...

A backtrace is the list of function calls that were in progress when the panic happened, most recent first. Read from the top down past the runtime's own frames (the panic_fmt machinery) until you reach your functions, and you'll see the path that led to the failure: here, main called cause_trouble, which panicked. This is the same call-stack picture from lesson 8.2, printed for you at the moment of failure. It's the single most useful debugging aid when a panic surprises you, and it costs nothing but typing the variable.

Tip

You don't need to set RUST_BACKTRACE permanently. Prefix a single run with it: RUST_BACKTRACE=1 cargo run. The debug builds you develop with (lesson 0.10) keep the information backtraces need; release builds may show less. When a panic baffles you, this is the first thing to reach for, ahead of scattering dbg! calls (lesson 3.4).

assert! and checked assumptions

Often you want to panic conditionally: state an assumption and have the program stop loudly if it's ever false. The assert! macro does exactly that. It takes a condition; if the condition is false, it panics with a message that includes the failed check:

fn withdraw(balance: u32, amount: u32) -> u32 {
    assert!(amount <= balance, "cannot withdraw {amount} from {balance}");
    balance - amount
}

fn main() {
    println!("{}", withdraw(100, 30));   // 70
    println!("{}", withdraw(100, 200));  // panics
}
70

thread 'main' panicked at src/main.rs:2:5:
cannot withdraw 200 from 100

assert! is how you encode an invariant: a fact that must hold for the code to make sense. Here, withdrawing more than the balance should never happen, so rather than silently underflow the u32 (lesson 4.4) we assert it can't and panic with a clear message if it does. There are two relatives: assert_eq!(a, b) panics unless a == b and prints both values when they differ (the workhorse of tests, chapter 14), and assert_ne! for "must not be equal."

debug_assert! is the same as assert! but only runs in debug builds. Use it for expensive checks you want during development but not in the shipped release binary, the same debug-versus-release split lesson 0.10 drew.

Key insight

A panic is for bugs, not for expected problems. "The user typed nonsense" is expected and recoverable, so it deserves a Result (next lesson), not a panic. "This index is somehow past the end of an array I just built" is a bug, a broken assumption, so it deserves a panic that stops the program before the bug does damage. The whole chapter hinges on telling these two apart, a judgment lesson 12.5 makes explicit.

The connection to unwrap

Now lesson 11.3's unwrap and expect make complete sense: they are convenience methods that panic when there's nothing to unwrap. option.unwrap() is "give me the value or panic!," and option.expect("msg") is "give me the value or panic! with this message." They convert a recoverable-looking Option into an unrecoverable panic, which is why lesson 11.3 warned against using them when None is a real possibility. Every unwrap is a panic! waiting for the wrong input, and you'll see the exact same machinery on Result next lesson.

Quiz time

Question #1

What three things does a panic print by default, and what does RUST_BACKTRACE=1 add?

Show solution

By default a panic prints the message, the file-and-line where it occurred, and a note suggesting RUST_BACKTRACE. Setting RUST_BACKTRACE=1 adds the backtrace: the chain of function calls in progress when the panic fired, most recent first, so you can trace the path to the failure.

Question #2

Rewrite this to panic with a clear message if divisor is zero, using assert!:

fn divide(value: i32, divisor: i32) -> i32 {
    value / divisor
}
Show solution
fn divide(value: i32, divisor: i32) -> i32 {
    assert!(divisor != 0, "divisor must not be zero");
    value / divisor
}

Without the assertion, divide(5, 0) panics anyway (integer division by zero, lesson 3.1), but with a generic message. The assert! fails earlier with a message you wrote, which is friendlier to whoever reads the panic.

Question #3

Is "the user typed abc when asked for a number" a good reason to panic? Why or why not?

Show solution

No. Bad user input is expected and recoverable: the program can print "please enter a number" and ask again. Panicking would crash the program over something perfectly normal. That situation calls for Result (next lesson) and the input-handling loop of lesson 12.4. Panic is for broken assumptions and bugs, not for inputs you should anticipate.

Panic handles the failures you can't recover from. The next lesson introduces the type for the ones you can: Result, which (you may have guessed) is just an enum, and the chapter 11 toolkit handles it on sight.