Step 6 of 6
thiserror or anyhow? Both.
Now the decision you'll make on every project:
| thiserror | anyhow | |
|---|---|---|
| Use it in | libraries, and modules other code calls | applications: main, CLIs, services, scripts, tests |
| Error type | your enum, one variant per failure | one anyhow::Error for everything |
| Callers can | match on variants and react |
report it (and rarely, downcast) |
| You get | Display, Error, From impls |
? on anything, context, a good main |
The rule of thumb: will the caller handle different failures differently? Then they need variants to match on: thiserror. Will the error just be reported? anyhow.
Most real projects use both: the library part (a lib.rs, or modules like inventory or db) defines thiserror enums, and the application part uses anyhow on top. They fit together: a thiserror enum implements std::error::Error, so ? converts it into anyhow::Error, and context adds the application's story.
mod inventory {
use std::collections::BTreeMap;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum InventoryError {
#[error("unknown item {0:?}")]
UnknownItem(String),
#[error("only {available} {item} left, asked for {wanted}")]
OutOfStock { item: String, available: u32, wanted: u32 },
}
#[derive(Default)]
pub struct Inventory {
stock: BTreeMap<String, u32>,
}
impl Inventory {
pub fn add(&mut self, item: &str, qty: u32) {
*self.stock.entry(item.to_string()).or_insert(0) += qty;
}
pub fn take(&mut self, item: &str, wanted: u32) -> Result<u32, InventoryError> {
let available = self
.stock
.get_mut(item)
.ok_or_else(|| InventoryError::UnknownItem(item.to_string()))?;
if *available < wanted {
return Err(InventoryError::OutOfStock { item: item.to_string(), available: *available, wanted });
}
*available -= wanted;
Ok(*available)
}
}
}
use anyhow::{Context, Result};
use inventory::{Inventory, InventoryError};
fn ship(inv: &mut Inventory, order: &str) -> Result<u32> {
let (item, qty) = order.split_once(' ').context("expected <item> <qty>")?;
let qty: u32 = qty.parse().with_context(|| format!("bad quantity {qty:?}"))?;
let left = inv.take(item, qty).with_context(|| format!("shipping {order:?}"))?;
Ok(left)
}
fn main() {
let mut inv = Inventory::default();
inv.add("mug", 3);
for order in ["mug 2", "mug 5", "lamp 1", "mug two"] {
match ship(&mut inv, order) {
Ok(left) => println!("shipped {order}, {left} left"),
Err(e) => match e.downcast_ref::<InventoryError>() {
Some(InventoryError::OutOfStock { .. }) => println!("backorder: {e:#}"),
_ => println!("error: {e:#}"),
},
}
}
}
shipped mug 2, 1 left
backorder: shipping "mug 5": only 1 mug left, asked for 5
error: shipping "lamp 1": unknown item "lamp"
error: bad quantity "two": invalid digit found in string
Piece by piece:
- The
inventorymodule knows nothing about anyhow. Its callers get a preciseInventoryErrorand could match on it directly. shipis application code: oneanyhow::Result,?on three different error types (Optionviacontext,ParseIntError,InventoryError), and context on each.e.downcast_ref::<InventoryError>()asks "is the error inside thisanyhow::ErroranInventoryError?" and returnsOption<&InventoryError>. It sees through the context layers. Here an out-of-stock item is a backorder, not a failure.- Downcasting is the escape hatch, not the plan: if you find yourself downcasting a lot, that layer should return a typed error instead.
Panics, once more
Errors are for things that can go wrong in a correct program: bad input, missing files, network failures. Panics are for bugs: an invariant your own code broke. At work: libraries never panic on bad input, expect("reason") documents why something can't fail, and many teams turn on clippy's unwrap_used lint in library code so reviewers see every place that could crash.
Common mistakes
- Returning
anyhow::Errorfrom a library (callers can't match) or writing a 12-variant enum for a CLI'smain(nobody matches on it). Match the tool to the caller. - Downcasting to the wrong type.
downcast_ref::<std::num::ParseIntError>()on an error whose outermost real error is your enum (with the parse error as its source) returnsNone. Downcast to the type that?converted, or walke.chain().
Your task
Finish this inventory tool. First give InventoryError the thiserror derive and messages: UnknownItem prints unknown item "lamp" (Debug-format the name) and OutOfStock prints only 1 mug left, asked for 5. Then write apply, which runs one command line and returns a message:
add <item> <qty>adds stock and returnsadded 3 mugtake <item> <qty>returnstook 2 mug, 1 left; itsInventoryErrorgoes up with a plain?(no extra context)- anything else:
bail!("unknown command {cmd:?}"); a missing part:bail!("expected: <command> <item> <qty>"); a bad quantity:?with contextbad quantity "x"
main is written: it prints backorder: ... when the error is an OutOfStock and error: ... otherwise.
Challenge 2
Map errors to exit codes (the BSD sysexits convention many tools follow). Write fn exit_code(e: &anyhow::Error) -> u8 that downcasts, checking in this order: a ConfigError gives 78, a std::io::Error gives 74, a std::num::ParseIntError gives 65, anything else 1. The errors may have context layers around them.
Challenge 3
Code review: this library function panics on bad input, which a library must never do. Make it return Result<Rgb, ColorError> instead. Define ColorError with thiserror: MissingHash (color must start with #), WrongLength(usize) (expected 6 hex digits, got 4), and BadDigit wrapping a std::num::ParseIntError with #[from] (invalid hex digit). No unwrap, expect or panic! left. (You may assume the input is ASCII, so slicing hex[0..2] is safe.)