Skip to content
Free public beta until December 4, 2026: every course is open, no card needed. After that, the first chapter of each course stays free and full access is a one-time $39 ($19 for up to 100 founding members chosen for helpful feedback). Your progress is kept. How the beta works

Every way to handle a failure

panic!, Option, Result, unwrap and expect, ?, your own error enum, Box and anyhow. What each one says about who can fix the problem.

Start with one question: who can do something about this failure?

  • Nobody: it's a bug (an index you proved is in range turns out not to be, an invariant broke) → panic. unwrap, expect with a reason, unreachable!, or an assert!.
  • Nothing went wrong; there's just no answer (no user with that id, an empty list has no maximum) → return Option<T>.
  • Something went wrong and the caller should decide (a file is missing, text doesn't parse) → return Result<T, E>.
  • This function can't decide either → ?, to pass the error up one level. Each level adds context if it knows something useful.
  • You're writing a library, and callers will want to react to particular failures → your own error enum, one variant per case they can act on (often with thiserror).
  • You're writing an application, and failures end in a message to a person → Box<dyn Error> or anyhow, with context attached on the way up and reported once, at the top, in main.

Panicking isn't wrong. It's the right answer when continuing would be worse. What's wrong is panicking on input a user could get wrong.

Stop the program

Section

Panics

What a panic prints, what still runs (drops), and how to read the backtrace.

Tests, prototypes, and cases you've proved can't fail. Otherwise, a crash waiting to happen.

Formal. When an integer operation's true result doesn't fit its type, a debug build (overflow checks on, the default for cargo run) panics with "attempt to add with overflow"; a release build (overflow checks off) wraps around modulo 2ⁿ. The explicit methods checked_*, wrapping_*, saturating_* and overflowing_* behave the same in both.

In plain English. If a number gets too big for its box, your debug build stops loudly so you notice, and the optimized build quietly wraps around. When you care, say what you want with a method such as checked_add.

Example. For let x: u8 = 255;, x + 1 panics in a debug build and is 0 in a release build; x.checked_add(1) is None in both.

Why this rule exists. Silent wraparound is a classic source of security bugs, but checking every addition costs speed in the final program. Rust checks where it's cheap to find bugs and lets you choose explicitly where it matters.

A failure you didn't write: overflow panics in debug builds and wraps in release.

Return a value that might not be there

Section

Option

Some(value) or None, with no null anywhere in the language.

The short ways to act on the one case you care about.

Return an error the caller handles

Ok or Err, and the combinators (map_err, unwrap_or, ok_or) that save a match.

Section

The ? operator

Return the error early, or carry on with the value.

Formal. For a Result, expr? evaluates to the Ok value, or returns early from the enclosing function with the Err value, first converting it with From::from into the function's error type. For an Option, it evaluates to the Some value or returns None. It may only be used in a function (or closure) whose return type is a compatible Result or Option.

In plain English. The question mark means "if this worked, give me the value and keep going; if it failed, stop here and pass the failure back to whoever called me."

Example. let text = std::fs::read_to_string(path)?; inside a function returning Result<String, std::io::Error> either gives text or returns the I/O error. Inside a function returning () it is error E0277.

Why this rule exists. It keeps error handling visible (every fallible call is marked) without burying the program's logic in match boilerplate.

What ? really expands to, including the From conversion that lets errors change type on the way up.

main can return a Result, so ? works all the way to the top.

The decision rule above, with the cases that argue for each.

Design the error type

One variant per failure a caller might treat differently.

Let ? turn a lower-level error into yours automatically.

Section

Box

Any error at all, when the caller only needs to show it.

thiserror for a library's precise types, anyhow for an application's context-rich messages.

A message added to an error to say what the program was doing.

Example

fs::read_to_string(&path).with_context(|| format!("reading {}", path.display()))? turns "No such file" into "reading settings.toml: No such file".

Tell a person

Print the whole chain once, at the top, on standard error.

A command-line tool's message, and the exit code a script can test.

The same error turned into an HTTP status and a body a client can read.

C can't see a Result or survive a panic, so both become status codes.