A Brief Introduction to thiserror / anyhow
Goal of This Episode
Meet the community’s two most popular error-handling crates.
Concept
This episode covers not the standard library, but two community crates. They’re practically standard equipment in the Rust ecosystem and extremely useful, so we introduce them here.
Install them before use:
cargo add thiserror
cargo add anyhow
Background
Last episode we saw how much boilerplate a custom error takes (enum + Display + Error + a From for each kind). thiserror and anyhow solve exactly that.
thiserror: For Libraries
thiserror auto-generates Display, Error, and From with a derive macro:
extern crate thiserror;
use thiserror::Error;
#[derive(Debug, Error)]
enum AppError {
#[error("I/O operation failed")]
Io(#[from] std::io::Error),
#[error("failed to parse an integer")]
Parse(#[from] std::num::ParseIntError),
#[error("custom error: {0}")]
Custom(String),
}
fn main() {}
#[error("...")]auto-generates theDisplayimplementation.#[from]auto-generates theFromimplementation and treats the same field as the underlying cause returned by.source().- What took dozens of hand-written lines last episode is now a few lines.
Usage is the same as last episode — ? converts automatically:
extern crate thiserror;
use thiserror::Error;
#[derive(Debug, Error)]
enum AppError {
#[error("I/O operation failed")]
Io(#[from] std::io::Error),
#[error("failed to parse an integer")]
Parse(#[from] std::num::ParseIntError),
#[error("custom error: {0}")]
Custom(String),
}
fn read_number(path: &str) -> Result<i32, AppError> {
let content = std::fs::read_to_string(path)?;
let num = content.trim().parse::<i32>()?;
Ok(num)
}
fn main() {}
Callers can still match and handle each error kind precisely.
anyhow: For Applications
If callers don’t need to distinguish error kinds (say, in the main function or a CLI tool), anyhow is even simpler:
extern crate anyhow;
use anyhow::{Context, Result};
fn read_number(path: &str) -> Result<i32> {
let content = std::fs::read_to_string(path)
.context("failed to read the file")?;
let num = content.trim().parse::<i32>()
.context("failed to parse the number")?;
Ok(num)
}
fn main() {}
anyhow::Result<T>is justResult<T, anyhow::Error>.anyhow::Erroris similar toBox<dyn Error + Send + Sync>, but more convenient to use..context("...")adds extra explanation to an error, handy for debugging.- No new error type to define; errors implementing
Error + Send + Sync + 'staticconvert automatically.
How the Two Relate
thiserror: helps you define precise error types without the repetitive hand-written code. For libraries — users canmatchon your errors.anyhow: no separate error type to define; errors meeting the bounds above are handled uniformly through one type. For applications — you just need to report errors, not let others handle them programmatically.
They combine well: libraries define errors with thiserror, applications receive them all through anyhow.
Example Code
// this example shows anyhow in action
extern crate anyhow;
use anyhow::{Context, Result};
use std::fs;
fn read_config(path: &str) -> Result<(String, i32)> {
let content = fs::read_to_string(path)
.context("couldn't read the config file")?;
let mut lines = content.lines();
let name = lines.next()
.context("the config file is empty")?
.to_string();
let value = lines.next()
.context("missing second line")?
.trim()
.parse::<i32>()
.context("the second line isn't a valid number")?;
Ok((name, value))
}
fn main() -> Result<()> {
let (name, value) = read_config("config.txt")?;
println!("name: {}, value: {}", name, value);
Ok(())
}
Recap
thiserror: auto-generatesDisplay,Error, andFromviaderive; suits libraries.#[error("...")]generatesDisplay;#[from]generatesFromand marks the field as the underlyingsource.anyhow: handles errors implementingError + Send + Sync + 'staticuniformly without an errorenum; suits applications..context("...")adds extra explanation to errors.- Libraries use
thiserror, applications useanyhow, and the two combine well.