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:
thiserrorhelps you define error types. You still write the enum, but a#[derive]writesDisplay,Error, andFromfor you.anyhowhelps 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”.
Libraries vs applications
Section titled “Libraries vs applications”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.
Setting up
Section titled “Setting up”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 foundinsufficient funds: balance is 50, tried to take 80amount must be greater than zeroInsufficientFunds { balance: 50, amount: 80 }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.
thiserror: #[from] and #[source]
Section titled “thiserror: #[from] and #[source]”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 scopeuse 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: 8080error: missing `=` in line "port 8080"error: the port is not a valid number caused by: invalid digit found in stringSometimes 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: 60line 2 does not contain a numbercaused by: invalid digit found in stringanyhow: one Result type for everything
Section titled “anyhow: one Result type for everything”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.5error: invalid digit found in stringerror: invalid float literalWithout 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 needsformat!.
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 settingscould not load settings: `99999` is not a valid port: number too large to fit in target type0: could not load settings1: `99999` is not a valid port2: number too large to fit in target typeThe 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 typeIf 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}"))?;anyhow: bail!, ensure!, and anyhow!
Section titled “anyhow: bail!, ensure!, and anyhow!”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 forreturn 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 zeroerror: insufficient funds: balance is 100, tried to take 250Ferriserror: no user with id 7Returning anyhow::Result from main
Section titled “Returning anyhow::Result from main”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(())}Nothing is printed to stdout. On stderr you see:
Error: could not read "forty-two" as a number
Caused by: invalid digit found in stringUsing both together
Section titled “Using both together”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 doneerror: order #2 failed: unknown item `plum`error: order #3 failed: only 3 left, cannot take 9 hint: try ordering 3 or fewerapples left: 3Common mistakes
Section titled “Common mistakes”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 scopehelp: 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-writtenDisplayandErrorimpls. #[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, andanyhow!creates one.- Common setup: thiserror in library code, anyhow in
main.
Quick check
Section titled “Quick check”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
Read more
Section titled “Read more”- anyhow documentation — docs.rs: the full API, including
Context,chain(), and downcasting. - thiserror documentation — docs.rs: every attribute, such as
#[error(transparent)]and backtrace fields. - The Context trait — anyhow: how
contextandwith_contextwork, with examples. - Error Handling — The Rust Book: the foundations that both crates build on.
- std::error::Error: the standard trait behind
.source()and error chains.