panics, unwinding and catch_unwind

A panic is a bug, not an error the caller can handle, and the distinction decides where each failure belongs: a wrong index is a panic, a missing file is a value. It unwinds the stack, runs the destructors and ends the thread or the process with status 101. Rust uses it for the cases the type system could not rule out: an index out of bounds, an unwrap on None, an assertion that failed.

The lesson shows the macro, the two shortcuts that produce a panic from a Result, the way a test catches a panic, one, and the exit code alternative for a program that wants to report without a message. The panic text itself is read at the end, because every line of it means something. The thread id in brackets changes per run.

panic!

panic! takes a message and starts the unwind. The message, the file and the position go to standard error, and the process ends with 101 unless a handler catches it.

The program prints one line, panics and stops, and the shell reports the status after it. The status after it is 101, which the shell shows with $?.

fn main() { println!("before"); panic!("something went wrong"); }

rustc --edition 2024 panic.rs
./panic
echo "status: $?"

before

thread 'main' (1612) panicked at panic.rs:3:5:
something went wrong
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
status: 101

The two blank lines and the note are part of the format; the note even names the variable that would print a backtrace. The lesson on gdb reads a panic from the debugger side.

unwrap and expect

unwrap turns an Option or a Result into its value and panics on the empty case. expect does the same with a message, which is the difference between a debug line and a panic worth reporting.

The program asks for an element that does not exist and expects a number from text that is not one. Both panics name the call and the position; the second adds the message.

fn main() { let values = [1, 2, 3]; let index: usize = "9".parse().unwrap(); println!("{}", values.get(index).unwrap()); }

fn main() { let text = "not a number"; let number: i32 = text.parse().expect("the input must be a number"); println!("{number}"); }

rustc --edition 2024 unwrap.rs
./unwrap
rustc --edition 2024 expect.rs
./expect


thread 'main' (3358) panicked at unwrap.rs:4:38:
called `Option::unwrap()` on a `None` value
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
status: 101

thread 'main' (3359) panicked at expect.rs:3:36:
the input must be a number: ParseIntError { kind: InvalidDigit }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
status: 101

The message after expect is the one to write for a reader who sees it in a crash report. The lesson on Result shows the question mark that avoids the panic altogether.

catch_unwind

catch_unwind runs a closure and returns Err when it panicked, which is how a test runner survives a failing test. The default panic hook still prints the message, so the output shows the panic and then the result.

The program catches a deliberate panic and prints whether it was caught. The panic text appears because catching does not silence the hook.

fn main() { let result = std::panic::catch_unwind(|| { panic!("inside"); }); println!("{}", result.is_err()); }

rustc --edition 2024 catch.rs
./catch


thread 'main' (1614) panicked at catch.rs:3:9:
inside
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
true
status: 0

A caught panic is not a recovery mechanism: the closure may have left a lock poisoned or a value half-written. It exists for isolation, which is the test runner's job.

An exit code instead

A program that fails for a reason it understands should print a message and return a status, not panic. std::process::exit ends the process at once with the number given.

The program exits with three and prints nothing, so the number is the whole message. The shell reports the number, which is what a script checks.

fn main() { std::process::exit(3); }

rustc --edition 2024 status.rs
./status
echo "status: $?"

status: 3

exit does not run destructors, which is the difference from returning from main. The lesson on functions shows the ExitCode return that does.

What the message contains

A panic line has the thread name, the thread id in brackets, the file, the line and the column, then the message on the next line. The note about RUST_BACKTRACE follows, and a caught panic prints the same block before the catch returns.

The file and line are the same in every build of the same source, and the thread id is the only part that changes between runs on one machine, which is why this page shows the outputs it got and the quiz questions avoid panic text. A message should say what was expected and what arrived.

The lesson on error types turns the cases a program can recover from into values, so that a panic is left for the cases it cannot.

Exercises

  1. Replace the unwrap in the second program with a match and print a message on the empty case.
  2. Catch a panic around a division by zero and print the result after it.
  3. Make a program exit with 2 on a bad argument and check the status in the shell.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What is a panic for?

    Answer

    A bug the program cannot recover from

    Recoverable failures are Results; a panic marks a broken invariant.

  2. What does unwrap do on a None?

    Answer

    It panics

    unwrap assumes the value is present and panics when it is not.

  3. What status does a panicking program leave?

    Answer

    101

    The unwinding runtime exits with 101 after printing the message.

  4. What does catch_unwind return?

    Answer

    Ok with the closure value, or Err when it panicked

    The result distinguishes a normal return from a caught panic.

  5. What does this program print? fn main() { let values = [1, 2, 3]; println!("{}", values.get(1).unwrap()); }

    Answer

    2

    Index one is present, and unwrap returns the value inside.

  6. What does this program print? fn main() { let value: Option<i32> = None; println!("{}", value.is_none()); println!("{}", value.unwrap_or(7)); }

    Answer

    true / 7

    The value is None, and unwrap_or supplies the fallback without a panic.

  7. What does this program print? fn main() { let result = std::panic::catch_unwind(|| { panic!("inside"); }); println!("{}", result.is_err()); }

    Answer

    true

    The closure panics and catch_unwind reports the Err.

  8. What does this program print? use std::process::ExitCode; fn main() -> ExitCode { println!("done"); ExitCode::FAILURE }

    Answer

    done

    The line prints, and the status is one without any message.

  9. What does this program print? fn main() { let text = "4"; let number: i32 = text.parse().expect("a number"); println!("{}", number + 1); }

    Answer

    5

    The parse succeeds, and expect returns the value, so the print is five.

  10. What kind of value should a recoverable failure be?

    Answer

    A Result the caller handles

    The Result keeps the decision with the caller and the reason in the type.

The code on this page was built with rustc 1.96.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev