Step 7 of 7
A first look at thiserror and anyhow
Writing Display, Error, source and From by hand is a lot of repetitive code. Two crates (Rust libraries) remove almost all of it, and you'll find them in most Rust codebases at work. Add them to a project's Cargo.toml:
[dependencies]
thiserror = "2"
anyhow = "1"
thiserror: error enums without the boilerplate
thiserror provides a derive macro, like #[derive(Debug)], that writes the Display, Error and From implementations for you:
use std::num::ParseIntError;
use thiserror::Error;
#[derive(Debug, Error)]
enum ConfigError {
#[error("missing '=' in line {0:?}")]
MissingEquals(String),
#[error("invalid number")]
BadNumber(#[from] ParseIntError),
}
fn parse_line(line: &str) -> Result<(String, u32), ConfigError> {
let (key, value) = line
.split_once('=')
.ok_or_else(|| ConfigError::MissingEquals(line.to_string()))?;
let value: u32 = value.trim().parse()?;
Ok((key.trim().to_string(), value))
}
fn main() {
for line in ["timeout = 30", "retries = lots", "verbose"] {
match parse_line(line) {
Ok((key, value)) => println!("{key} = {value}"),
Err(ConfigError::BadNumber(cause)) => println!("invalid number ({cause})"),
Err(e) => println!("error: {e}"),
}
}
}
timeout = 30
invalid number (invalid digit found in string)
error: missing '=' in line "verbose"
#[derive(Debug, Error)]:Errorhere is thiserror's derive (brought in byuse thiserror::Error;). It implementsstd::error::Errorfor the enum.#[error("...")]on each variant is its Display message.{0}refers to the first field of a tuple variant; named fields are used by name, like#[error("cannot buy {requested} tickets")]. Format specs like{0:?}work too.#[from]on a field writesimpl From<ParseIntError> for ConfigError, so?converts, and makessource()return that field.
It's still an ordinary enum: callers match on it exactly as before. That's why thiserror is the usual choice for libraries and for any module whose callers need to tell errors apart.
anyhow: one error type for applications
In application code you often don't match on errors at all: you add context and report them. anyhow gives you one type, anyhow::Error, that holds any error (like Box<dyn Error>, but with more features), and the alias anyhow::Result<T> for Result<T, anyhow::Error>.
use anyhow::{bail, Context, Result};
fn parse_port(text: &str) -> Result<u16> {
let port: u16 = text
.trim()
.parse()
.with_context(|| format!("invalid port {text:?}"))?;
if port < 1024 {
bail!("port {port} is reserved");
}
Ok(port)
}
fn main() {
for text in ["8080", "80", "http"] {
match parse_port(text) {
Ok(port) => println!("listening on {port}"),
Err(e) => println!("error: {e:#}"),
}
}
}
listening on 8080
error: port 80 is reserved
error: invalid port "http": invalid digit found in string
use anyhow::Result;replaces the standardResultin this file with anyhow's one-parameter version.Result<u16>meansResult<u16, anyhow::Error>..with_context(|| ...)(from theContexttrait) wraps the error with a message saying what you were doing. The original error becomes its source..context("fixed text")does the same with a fixed message.bail!("...")returns an error with that message right away: short forreturn Err(anyhow!("...")).{:#}prints the whole chain joined by:. Plain{}prints only the outermost message (invalid port "http").fn main() -> anyhow::Result<()>is a very common signature: an error at the end prints its full chain.
Which one?
- Library, or callers need to match on errors: thiserror. Your error enum is part of your API.
- Application code, where errors are reported rather than handled: anyhow, with plenty of
context. - Many projects use both: thiserror inside their library modules, anyhow in
mainand the command handlers.
Module 39 goes deep into both crates (ensure!, error chains in main, mixing them, designing error types for a real service).
Common mistakes
- A thiserror variant without
#[error(...)]. Then no Display gets written and the enum doesn't compile as an error. Every variant needs a message. - anyhow in a library's public API. Callers can't match on an
anyhow::Error, so they can't react to specific failures. Use it in applications. - Context that repeats the cause.
.context("parse error")adds nothing toinvalid digit found in string. Say what you were doing:.context("reading the port from config.toml").
Your task
Rewrite TicketError with thiserror. Add an #[error(...)] attribute to each variant so that:
SoldOutdisplays asthe event is sold outTooMany { requested: 9, max: 6 }displays ascannot buy 9 tickets, the limit is 6UnknownEvent("Rust Fest")displays asno event called 'Rust Fest'
Challenge 2
Write ScoreError with thiserror (derive Debug and Error):
MissingColon, displayed asexpected NAME:POINTSBadPoints, wrapping aParseIntErrorwith#[from], displayed aspoints are not a number
Then write fn parse_score(line: &str) -> Result<(String, u32), ScoreError> for lines like ferris:120 (trim both parts), using ? for the parse.
Challenge 3
Use anyhow. Write fn load_limit(text: &str) -> anyhow::Result<u32> that reads a rate limit:
- parse the trimmed text as a
u32; on failure add context withwith_contextso that the full chain ({:#}) islimit "abc" is not a number: invalid digit found in string(the original text, in quotes, via{text:?}) - a limit of
0fails withbail!and the messagelimit must be at least 1