Item 18: Don't panic

"It looked insanely complicated, and this was one of the reasons why the snug plastic cover it fitted into had the words DON’T PANIC printed on it in large friendly letters." – Douglas Adams

The title of this Item would be more accurately described as: prefer returning a Result to using panic! (but don't panic is much catchier).

The first thing to understand about Rust's panic system is that it is not equivalent to an exception system (like the ones in Java or C++), even though there appears to be a mechanism for catching panics at a point further up the call stack.

Consider a function that panics on an invalid input:

#![allow(unused)]
fn main() {
fn divide(a: i64, b: i64) -> i64 {
    if b == 0 {
        panic!("Cowardly refusing to divide by zero!");
    }
    a / b
}
}

Trying to invoke this with an invalid input fails as expected:

    // Attempt to discover what 0/0 is...
    let result = divide(0, 0);
thread 'main' panicked at 'Cowardly refusing to divide by zero!', panic/src/main.rs:11:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

A wrapper that uses std::panic::catch_unwind to catch the panic

fn divide_recover(a: i64, b: i64, default: i64) -> i64 {
    let result = std::panic::catch_unwind(|| divide(a, b));
    match result {
        Ok(x) => x,
        Err(_) => default,
    }
}

appears to work:

    let result = divide_recover(0, 0, 42);
    println!("result = {}", result);
result = 42

Appearances can be deceptive, however. The first problem with this approach is that panics don't always unwind; there is a compiler option (which is also accessible via a Cargo.toml profile setting) that shifts panic behaviour so that it immediately aborts the process.

thread 'main' panicked at 'Cowardly refusing to divide by zero!', panic/src/main.rs:11:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Abort trap: 6

This leaves any attempt to simulate exceptions entirely at the mercy of the wider project settings. It's also the case that some target platforms (for example WebAssembly) always abort on panic, regardless of any compiler or project settings.

A more subtle problem that's surfaced by panic handling is exception safety: if a panic occurs midway through an operation on a data structure, it removes any guarantees that the data structure has been left in a self-consistent state. Preserving internal invariants in the presence of exceptions has been known to be extremely difficult since the 1990s 1; this is one of the main reasons why Google (famously) bans the use of exceptions in its C++ code.

Finally, panic propagation also interacts poorly with FFI (foreign function interface) boundaries (Item 34); use catch_unwind to prevent panics in Rust code from propagating to non-Rust calling code across an FFI boundary.

So what's the alternative to panic! for dealing with error conditions? For library code, the best alternative is to make the error someone else's problem, by returning a Result with an appropriate error type (Item 4). This allows the library user to make their own decisions about what to do next – which may involve passing the problem on to the next caller in line, via the ? operator.

The buck has to stop somewhere, and a useful rule of thumb is that it's OK to panic! (or to unwrap(), expect() etc.) if you have control of main; at that point, there's no further caller that the buck could be passed to.

Another sensible use of panic!, even in library code, is in situations where it's very rare to encounter errors, and you don't want users to have to litter their code with .unwrap() calls.

If an error situation should only occur because (say) internal data is corrupted, rather than as a result of invalid inputs, then triggering a panic! is legitimate.

It can even be occasionally useful to allow panics that can be triggered by invalid input, but where such invalid inputs are out of the ordinary. This works best when the relevant entrypoints come in pairs:

  • an "infallible" version whose signature implies it always succeeds (and which panics if it can't succeed),
  • a "fallible" version that returns a Result.

For the former, Rust's API guidelines suggest that the panic! should be documented in a specific section of the inline documentation (Item 27).

The String::from_utf8_unchecked / String::from_utf8 entrypoints in the standard library are an example of the latter (although in this case, the panics are actually deferred to the point where a String constructed from invalid input gets used…).

Assuming that you are trying to comply with the advice of this item, there are a few things to bear in mind. The first is that panics can appear in different guises; avoiding panic! also involves avoiding:

Harder to spot are things like:

  • slice[index] when the index is out of range
  • x / y when y is zero.

The second observation around avoiding panics is that a plan that involves constant vigilance of humans is never a good idea.

However, constant vigilance of machines is another matter: adding a check to your continuous integration (Item 32) system that spots new panics is much more reliable. A simple version could be a simple grep for the most common panicking entrypoints (as above); a more thorough check could involve additional tooling from the Rust ecosystem (Item 31), such as setting up a build variant that pulls in the no_panic crate.


1: Tom Cargill's 1994 article in the C++ Report explored just how difficult exception safety is for C++ template code, as did Herb Sutter's Guru of the Week #8 column.