Skip to content

Rust Error Handling with anyhow and thiserror

On the Result and Custom Error Types pages, you wrote error types by hand: an enum, an impl Display, maybe an impl From so that ? works. That is the right way to learn. But in a real project you write the same boilerplate again and again.

Two small crates, both by the same author, remove that boilerplate. They solve two different problems:

  • thiserror helps you define error types. You still write the enum, but a #[derive] writes Display, Error, and From for you.
  • anyhow helps you pass errors along. It gives you one error type that can hold any error, plus easy ways to add a message like “while loading the config file”.

Which one you pick depends on who handles the error.

A library is code other people call. Its callers may want to react differently to each failure: retry on a timeout, show a form error on bad input. So a library should return a precise enum that callers can match on. That is thiserror.

An application (your main.rs) is usually the last stop. Most errors end up as a message for a human, and maybe an exit code. You rarely match on them; you want them easy to pass up with ? and easy to read. That is anyhow.

A good rule of thumb: thiserror for code that others call, anyhow for the top of your program. Many projects use both, as you’ll see at the end of this page.

Terminal window
cargo add thiserror anyhow
[dependencies]
anyhow = "1"
thiserror = "2"

New to Cargo.toml? See Cargo & Crates. Both crates are available in the Rust Playground, so every “Try it Yourself” button on this page works.

thiserror: derive Error and write messages

Section titled “thiserror: derive Error and write messages”

Put #[derive(Debug, Error)] on your enum, then give each variant an #[error("...")] message. The message becomes the variant’s Display text. Inside it, {0} means the first tuple field, and {balance} means a named field:

use thiserror::Error;
#[derive(Debug, Error)]
enum BankError {
#[error("account `{0}` not found")]
NotFound(String),
#[error("insufficient funds: balance is {balance}, tried to take {amount}")]
InsufficientFunds { balance: u32, amount: u32 },
#[error("amount must be greater than zero")]
ZeroAmount,
}
fn main() {
let errors = vec![
BankError::NotFound(String::from("alice")),
BankError::InsufficientFunds { balance: 50, amount: 80 },
BankError::ZeroAmount,
];
for e in &errors {
println!("{e}");
}
println!("{:?}", errors[1]);
}

Output:

account `alice` not found
insufficient funds: balance is 50, tried to take 80
amount must be greater than zero
InsufficientFunds { balance: 50, amount: 80 }
▶ Try it Yourself

This is exactly the enum from Custom Error Types, but the whole impl Display block is gone. Nothing is hidden at runtime: thiserror only writes the code you would have written by hand. Callers see a normal enum.

Errors often have a cause: “the port is invalid” because “invalid digit found in string”. The standard Error trait has a .source() method for this. thiserror fills it in for you.

#[from] does two things. It marks the field as the source, and it writes an impl From, so ? converts the inner error into your variant automatically:

use std::error::Error; // brings the .source() method into scope
use std::num::ParseIntError;
#[derive(Debug, thiserror::Error)]
enum ConfigError {
#[error("missing `=` in line {0:?}")]
NoEquals(String),
#[error("the port is not a valid number")]
BadPort(#[from] ParseIntError),
}
fn parse_port(line: &str) -> Result<u16, ConfigError> {
let (_, value) = line
.split_once('=')
.ok_or_else(|| ConfigError::NoEquals(line.to_string()))?;
let port = value.trim().parse::<u16>()?; // ParseIntError becomes ConfigError::BadPort
Ok(port)
}
fn main() {
for line in ["port = 8080", "port 8080", "port = eighty"] {
match parse_port(line) {
Ok(port) => println!("ok: {port}"),
Err(e) => {
println!("error: {e}");
if let Some(cause) = e.source() {
println!(" caused by: {cause}");
}
}
}
}
}

Output:

ok: 8080
error: missing `=` in line "port 8080"
error: the port is not a valid number
caused by: invalid digit found in string
▶ Try it Yourself

Sometimes you want a source but not an automatic From, because you need to add extra data, like a line number. Use #[source] on the field instead, and build the error yourself with .map_err(). (A field simply named source is treated as the source automatically.) thiserror also works on structs, not only enums:

use std::error::Error;
use std::num::ParseIntError;
#[derive(Debug, thiserror::Error)]
#[error("line {line} does not contain a number")]
struct LineError {
line: usize,
#[source]
cause: ParseIntError,
}
fn sum_lines(text: &str) -> Result<i32, LineError> {
let mut total = 0;
for (i, row) in text.lines().enumerate() {
let n: i32 = row
.trim()
.parse()
.map_err(|cause| LineError { line: i + 1, cause })?;
total += n;
}
Ok(total)
}
fn main() {
println!("total: {}", sum_lines("10\n20\n30").unwrap());
let err = sum_lines("10\ntwenty\n30").unwrap_err();
println!("{err}");
println!("caused by: {}", err.source().unwrap());
}

Output:

total: 60
line 2 does not contain a number
caused by: invalid digit found in string
▶ Try it Yourself

anyhow::Result<T> is short for Result<T, anyhow::Error>. An anyhow::Error can hold any error type that implements the standard Error trait. So ? works on all of them, even when one function hits several different error types:

use anyhow::Result;
// Two different error types: ParseIntError and ParseFloatError.
// anyhow::Result accepts both through `?`.
fn total_price(quantity: &str, price: &str) -> Result<f64> {
let quantity: u32 = quantity.trim().parse()?;
let price: f64 = price.trim().parse()?;
Ok(quantity as f64 * price)
}
fn main() {
for (quantity, price) in [("3", "2.50"), ("three", "2.50"), ("3", "cheap")] {
match total_price(quantity, price) {
Ok(total) => println!("total: {total}"),
Err(e) => println!("error: {e}"),
}
}
}

Output:

total: 7.5
error: invalid digit found in string
error: invalid float literal
▶ Try it Yourself

Without anyhow, you would need your own enum with two variants, or a Box<dyn Error>. anyhow is like a nicer Box<dyn Error>. But notice the problem: “invalid digit found in string” doesn’t say which value was wrong. That’s what context is for.

anyhow: adding context and printing the chain

Section titled “anyhow: adding context and printing the chain”

Import the Context trait, and every Result gets two new methods:

  • .context("message") wraps the error with a fixed message.
  • .with_context(|| format!(...)) takes a closure. It only builds the message if there really is an error, so use it when the message needs format!.

Each layer of context adds one link to an error chain. How you print the error decides how much of the chain you see:

use anyhow::{Context, Result};
fn parse_port(text: &str) -> Result<u16> {
let port: u16 = text
.parse()
.with_context(|| format!("`{text}` is not a valid port"))?;
Ok(port)
}
fn load_settings(port_text: &str) -> Result<u16> {
let port = parse_port(port_text).context("could not load settings")?;
Ok(port)
}
fn main() {
let err = load_settings("99999").unwrap_err();
println!("{err}"); // only the outermost message
println!("{err:#}"); // the whole chain on one line
for (i, cause) in err.chain().enumerate() {
println!("{i}: {cause}");
}
}

Output:

could not load settings
could not load settings: `99999` is not a valid port: number too large to fit in target type
0: could not load settings
1: `99999` is not a valid port
2: number too large to fit in target type
▶ Try it Yourself

The third format is {:?}. It prints a multi-line report, which is what you see when main returns an error:

println!("{err:?}");
could not load settings
Caused by:
0: `99999` is not a valid port
1: number too large to fit in target type

If the RUST_BACKTRACE=1 environment variable is set, {:?} also adds a stack backtrace showing where the error was created. See Environment Variables.

A typical real use is file reading, where the operating system’s message (“No such file or directory”) never says which file:

let text = std::fs::read_to_string(path)
.with_context(|| format!("failed to read config file {path}"))?;

Sometimes there is no underlying error. You just decide that something is wrong. anyhow has three macros for that:

  • bail!("...") returns an error right now. It is short for return Err(anyhow!("...")).
  • ensure!(condition, "...") returns an error if the condition is false.
  • anyhow!("...") creates an error value without returning it.

All three take format!-style messages.

use anyhow::{anyhow, bail, ensure, Result};
fn withdraw(balance: u32, amount: u32) -> Result<u32> {
ensure!(amount > 0, "amount must be greater than zero");
if amount > balance {
bail!("insufficient funds: balance is {balance}, tried to take {amount}");
}
Ok(balance - amount)
}
fn find_user(id: u32) -> Result<&'static str> {
match id {
1 => Ok("Ferris"),
_ => Err(anyhow!("no user with id {id}")),
}
}
fn main() {
println!("{:?}", withdraw(100, 30).ok());
for (balance, amount) in [(100, 0), (100, 250)] {
if let Err(e) = withdraw(balance, amount) {
println!("error: {e}");
}
}
println!("{}", find_user(1).unwrap());
println!("error: {}", find_user(7).unwrap_err());
}

Output:

Some(70)
error: amount must be greater than zero
error: insufficient funds: balance is 100, tried to take 250
Ferris
error: no user with id 7
▶ Try it Yourself

The most common place to see anyhow is here: main returns anyhow::Result<()>, and you use ? everywhere. If an error reaches main, Rust prints it with {:?} to stderr and exits with code 1:

use anyhow::{Context, Result};
fn main() -> Result<()> {
let text = "forty-two";
let n: i32 = text
.parse()
.with_context(|| format!("could not read {text:?} as a number"))?;
println!("{n}");
Ok(())
}
▶ Try it Yourself

Nothing is printed to stdout. On stderr you see:

Error: could not read "forty-two" as a number
Caused by:
invalid digit found in string

Here is the usual shape of a real project. The inventory module plays the part of a library: it returns a precise InventoryError made with thiserror. The application code uses anyhow, adds context, and only looks inside the error where it actually cares. downcast_ref asks an anyhow::Error “are you secretly this type?” and works even through added context:

// ----- "library" part: precise errors with thiserror -----
mod inventory {
use thiserror::Error;
#[derive(Debug, Error)]
pub enum InventoryError {
#[error("unknown item `{0}`")]
UnknownItem(String),
#[error("only {available} left, cannot take {wanted}")]
NotEnough { available: u32, wanted: u32 },
}
pub struct Store {
pub apples: u32,
}
impl Store {
pub fn take(&mut self, item: &str, wanted: u32) -> Result<(), InventoryError> {
if item != "apple" {
return Err(InventoryError::UnknownItem(item.to_string()));
}
if wanted > self.apples {
return Err(InventoryError::NotEnough { available: self.apples, wanted });
}
self.apples -= wanted;
Ok(())
}
}
}
// ----- "application" part: anyhow + context -----
use anyhow::{Context, Result};
use inventory::{InventoryError, Store};
fn process_order(store: &mut Store, id: u32, item: &str, amount: u32) -> Result<()> {
store
.take(item, amount)
.with_context(|| format!("order #{id} failed"))?;
println!("order #{id} done");
Ok(())
}
fn main() {
let mut store = Store { apples: 5 };
let orders = [(1, "apple", 2), (2, "plum", 1), (3, "apple", 9)];
for (id, item, amount) in orders {
if let Err(e) = process_order(&mut store, id, item, amount) {
println!("error: {e:#}");
// We can still look inside for the original thiserror type:
if let Some(InventoryError::NotEnough { available, .. }) =
e.downcast_ref::<InventoryError>()
{
println!(" hint: try ordering {available} or fewer");
}
}
}
println!("apples left: {}", store.apples);
}

Output:

order #1 done
error: order #2 failed: unknown item `plum`
error: order #3 failed: only 3 left, cannot take 9
hint: try ordering 3 or fewer
apples left: 3
▶ Try it Yourself

Forgetting to import Context. .context() is a method from the anyhow::Context trait, and trait methods only work when the trait is in scope:

use anyhow::Result;
fn parse(text: &str) -> Result<i32> {
let n: i32 = text.parse().context("not a number")?;
Ok(n)
}
error[E0599]: no method named `context` found for enum `Result<T, E>` in the current scope
--> src/main.rs:4:31
|
4 | let n: i32 = text.parse().context("not a number")?;
| ^^^^^^^
|
= help: items from traits can only be used if the trait is in scope
help: trait `Context` which provides `context` is implemented but not in scope; perhaps you want to import it
|
1 + use anyhow::Context;
|

Fix: use anyhow::{Context, Result};.

Repeating the source’s message in your own message. With #[from], the inner error is already the source. If you also put {0} in the message, a full report prints it twice:

#[derive(Debug, thiserror::Error)]
enum AppError {
#[error("invalid port: {0}")] // includes the source's text...
BadPort(#[from] ParseIntError), // ...and this is the source too
}

Printed with {:#} through anyhow, this gives invalid port: invalid digit found in string: invalid digit found in string. Fix: either write #[error("invalid port")] and let the chain show the cause, or keep {0} and don’t mark the field as a source. Pick one.

Printing with {} and losing the cause. println!("{e}") on an anyhow::Error shows only the outermost message, like “could not load settings”. When you print an error for a human, use {:#} (one line) or {:?} (full report).

Using anyhow in a library’s public API. If your library returns anyhow::Result, callers can’t match on your errors. They would have to guess the type with downcast_ref. Give public functions a thiserror enum, and keep anyhow for applications, tests, and examples.

  • thiserror is for defining errors: #[derive(Error)] plus an #[error("...")] message per variant replaces your hand-written Display and Error impls.
  • #[from] makes ? convert an inner error into your variant and records it as the source. #[source] records the source without the conversion.
  • anyhow is for passing errors up: anyhow::Result<T> accepts any error through ?.
  • .context() and .with_context() add a human-readable layer to the error chain. Print the chain with {:#} (one line) or {:?} (report).
  • bail! returns an error now, ensure! returns one if a condition is false, and anyhow! creates one.
  • Common setup: thiserror in library code, anyhow in main.

Quick check

1. You are writing a library, and callers need to react differently to a timeout and to bad input. Which crate fits best?

2. What does #[from] on a variant's field do?

3. Which format prints an anyhow::Error with its whole chain on one line?

4. When should you use .with_context(|| ...) instead of .context(...)?

5. What does ensure!(amount > 0, "amount must be positive") do when amount is 0?

Score: 0 / 5