---
title: "12.1 panic! and unrecoverable errors"
description: "What panicking actually does, reading a backtrace with RUST_BACKTRACE, and assert!/debug_assert! as the panic machinery behind checked assumptions."
url: "https://learnrust.net/chapter-12/panic-and-unrecoverable-errors/"
last_updated: "2026-06-13"
---

# 12.1 panic! and unrecoverable errors

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](https://learnrust.net/chapter-4/integer-overflow/)), an out-of-range slice (lesson [9.6](https://learnrust.net/chapter-9/slices/)), `unwrap` on a `None` (lesson [11.3](https://learnrust.net/chapter-11/option/)). All of those call the same underlying mechanism, and you can call it directly with the `panic!` macro:

```rust
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](https://learnrust.net/chapter-8/the-stack-and-the-heap/)), dropping every value along the way so cleanup still runs (lesson [8.3](https://learnrust.net/chapter-8/scope-and-drop/) 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](https://learnrust.net/chapter-7/halting-your-program-early/)).

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](https://learnrust.net/chapter-1/reading-compiler-errors/) 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:

```bash
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](https://learnrust.net/chapter-8/the-stack-and-the-heap/), 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](https://learnrust.net/chapter-0/build-configurations/)) 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](https://learnrust.net/chapter-3/print-debugging/)).

## 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:

```rust
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](https://learnrust.net/chapter-4/integer-overflow/)) 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](https://learnrust.net/chapter-0/build-configurations/) 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](https://learnrust.net/chapter-12/when-to-panic/) makes explicit.

## The connection to unwrap

Now lesson [11.3](https://learnrust.net/chapter-11/option/)'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](https://learnrust.net/chapter-11/option/) 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?

<details class="solution">
<summary>Show solution</summary>

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.

</details>

**Question #2**

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

```rust
fn divide(value: i32, divisor: i32) -> i32 {
    value / divisor
}
```

<details class="solution">
<summary>Show solution</summary>

```rust
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](https://learnrust.net/chapter-3/syntax-and-semantic-errors/)), but with a generic message. The `assert!` fails earlier with a message you wrote, which is friendlier to whoever reads the panic.

</details>

**Question #3**

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

<details class="solution">
<summary>Show solution</summary>

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](https://learnrust.net/chapter-12/handling-invalid-input/). Panic is for broken assumptions and bugs, not for inputs you should anticipate.

</details>

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.

## Sitemap

See the full [sitemap](https://learnrust.net/sitemap.md) for all pages.
