Rust Error Handling

Rust has no exceptions and no null. Operations that can fail return Result<T, E>, either Ok(value) or Err(error), and values that might be absent are Option<T>, either Some(value) or None. The type system forces you to deal with both cases before you can use the value, so forgetting to handle an error is a compile error, not a crash in production.

The ? operator keeps this from becoming verbose: it returns early with the error if there is one and unwraps the value otherwise. Combined with two popular crates (thiserror for defining library errors and anyhow for application-level errors), Rust error handling is both rigorous and concise.

TL;DR

Quick Example

A small application using anyhow for context and ? for propagation:

If the file is missing, the program exits with a readable chain:

Core Concepts

Result and Option

Both are ordinary enums. You handle them with match, if let, let ... else, or combinators:

Convert between them with .ok() (Result → Option) and .ok_or(err) / .ok_or_else(|| err) (Option → Result).

The ? Operator

? on a Result returns Err(e) from the current function (after converting e with From::from) or evaluates to the Ok value. On an Option, it returns None early. The function's return type must be compatible, which is why main can return Result<()>.

Custom Error Types

Libraries should expose errors callers can inspect. The idiomatic shape is an enum implementing std::error::Error, which thiserror derives for you:

Callers can then match on variants: return 404 for NotFound, 409 for Conflict, 500 for Database.

Error Sources and Chains

The Error trait's source() method links an error to its cause, forming a chain like Go's wrapped errors (see Go error handling). #[from] and #[source] in thiserror set it; anyhow's .context() adds a layer on top. Reporters print the whole chain.

thiserror vs anyhow

Many projects use both: thiserror inside library crates and domain modules, anyhow at the application edges where errors are logged and reported.

Panic vs Result

panic! unwinds (or aborts) the current thread. Use it for:

Don't panic on anything a caller could reasonably handle: bad input, missing files, network failures. Libraries in particular should almost never panic on user input.

Best Practices

Use expect With a Reason Over unwrap

If you must assert success, .expect("config validated at startup") states the invariant and makes panics diagnosable. Lint with clippy::unwrap_used in production crates.

Add Context at Each Layer

A bare No such file or directory is useless. Add what the program was trying to do and with what: reading config file app.toml. Context belongs where the knowledge is: the function that knows the path adds the path.

Keep Library Errors Stable and Specific

Public error enums are part of your API. Mark them #[non_exhaustive] so you can add variants without a breaking change, and avoid leaking every dependency's error type as a public variant.

Map Errors at Boundaries

Convert domain errors into HTTP responses, exit codes, or user messages in one place, such as an IntoResponse impl in Axum (see Axum). Don't scatter status-code logic through business code.

Common Mistakes

Unwrapping in Production Code

Stringly-Typed Errors

Result<T, String> loses the source chain and makes matching impossible. Use an error enum or anyhow instead.

Discarding Errors With let _ = or .ok()

let _ = file.flush(); silently hides failures. If ignoring an error is intentional, comment why; otherwise handle or propagate it.

FAQ

Why doesn't Rust have exceptions?

Exceptions create invisible control flow and make it easy to forget handling. Rust puts failure in the type signature: a function returning Result visibly can fail, and callers must acknowledge it. ? keeps propagation to a single character, so explicitness doesn't cost much verbosity.

Should I use thiserror or anyhow?

thiserror for libraries and any code whose callers need to react differently to different failures. anyhow for applications where errors are mostly reported, not matched. Using thiserror in your domain crates and anyhow in main is a common, sensible split.

What's Box<dyn Error>?

A trait object that can hold any error type. It's a quick, dependency-free catch-all for small programs and examples, and ? converts most errors into it automatically. anyhow is a more capable version of the same idea, with context and backtraces.

How do I get a backtrace?

anyhow captures backtraces when RUST_BACKTRACE=1 (or RUST_LIB_BACKTRACE=1) is set. For panics, RUST_BACKTRACE=1 prints the stack at the panic site. std's std::backtrace::Backtrace can be captured manually in custom error types.

Related Topics

References