anyhow crate handles errors in application code with almost no ceremony;anyhow::Result<T> and anyhow::Error let ? accept any error type;.context() and .with_context() attach human-readable context to an error;anyhow!, bail! and ensure! macros create and return ad-hoc errors;anyhow versus thiserror, and what anyhow gives you over plain Box<dyn Error>.Box<dyn Error> and thiserror;Learn Rust Series):We reach the halfway mark of the planned series -- episode fifty, quite a milestone -- and we close the error-handling arc with anyhow, the natural counterpart to last episode's thiserror. If you remember the mental model I hammered on at the end of episode 49, it splits the world neatly in two: precise errors for libraries, flexible errors for applications. thiserror sits on the library side, where a caller needs a concrete, matchable enum to react differently to each failure mode. anyhow sits on the application side, where you mostly want to propagate an error up to main, attach some context along the way, and print a clean report when things go wrong.
Having said that, the two are not rivals -- they are partners, and mature projects reach for both. Today we learn what anyhow gives you, we keep proving (as always) that none of it is magic by writing the plain std equivalent right next to it, and we finish with a comparison to how Python and Go handle this same problem. No hand-waving allowed ;-)
Episode 49 was thiserror. Here are the three exercises worked out.
Exercise 1 asked you to rewrite AppError with #[derive(Error)], #[error] and #[from], then add a fresh variant wrapping std::io::Error with #[from] and confirm it propagates through ? with no map_err. In thiserror that is delightfully short:
// requires the `thiserror` crate: shown for illustration, not compiled locally
use thiserror::Error;
use std::num::ParseIntError;
#[derive(Error, Debug)]
enum AppError {
#[error("could not parse a number")]
Parse(#[from] ParseIntError), // generates From
#[error("i/o failure")]
Io(#[from] std::io::Error), // generates From
}
fn load(path: &str) -> Result<i32, AppError> {
let text = std::fs::read_to_string(path)?; // io::Error -> AppError, no map_err
Ok(text.trim().parse()?) // ParseIntError -> AppError, no map_err
}
And here is the exact same thing hand-written in plain std, which is what the two #[from] attributes expand to -- this one compiles:
use std::fmt;
use std::num::ParseIntError;
#[derive(Debug)]
enum AppError { Parse(ParseIntError), Io(std::io::Error) }
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
AppError::Parse(_) => write!(f, "could not parse a number"),
AppError::Io(_) => write!(f, "i/o failure"),
}
}
}
impl std::error::Error for AppError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self { AppError::Parse(e) => Some(e), AppError::Io(e) => Some(e) }
}
}
impl From<ParseIntError> for AppError { fn from(e: ParseIntError) -> AppError { AppError::Parse(e) } }
impl From<std::io::Error> for AppError { fn from(e: std::io::Error) -> AppError { AppError::Io(e) } }
fn load(path: &str) -> Result<i32, AppError> {
let text = std::fs::read_to_string(path)?; // io::Error -> AppError via From
Ok(text.trim().parse()?) // ParseIntError -> AppError via From
}
fn main() { println!("{}", load("/definitely/missing").is_err()); } // true
Exercise 2 wanted a struct-form variant NotFound { key, table } whose #[error("...")] message interpolates both fields and reads like a sentence a user could act on. The generated Display is just an ordinary write! with the two named fields, so here is the compiled std shape of it:
use std::fmt;
#[derive(Debug)]
enum StoreError { NotFound { key: String, table: String } }
impl fmt::Display for StoreError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
StoreError::NotFound { key, table } =>
write!(f, "no record with key '{key}' in table '{table}'"),
}
}
}
fn main() {
let e = StoreError::NotFound { key: "42".into(), table: "users".into() };
println!("{e}"); // no record with key '42' in table 'users'
}
Exercise 3 asked for a catch-all #[error(transparent)] variant over Box<dyn Error + Send + Sync>, plus the hand-rolled equivalent, and a comparison of the line counts. The thiserror version is one attribute and one line; the hand-written version forwards Display and source to the inner error:
use std::fmt;
use std::error::Error;
#[derive(Debug)]
enum AppError { Other(Box<dyn Error + Send + Sync>) }
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self { AppError::Other(e) => write!(f, "{e}") } // forward inner message
}
}
impl Error for AppError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self { AppError::Other(e) => Some(e.as_ref()) }
}
}
impl From<Box<dyn Error + Send + Sync>> for AppError {
fn from(e: Box<dyn Error + Send + Sync>) -> AppError { AppError::Other(e) }
}
fn main() {
let inner: Box<dyn Error + Send + Sync> = "boom".into();
println!("{}", AppError::Other(inner)); // boom
}
Eleven lines by hand versus two with transparent and #[from], and they behave identically. That gap -- twenty-ish lines of Display/Error/From collapsing into a handful of attributes -- is exactly the ceremony thiserror erases. Now let's look at the other half of the story, anyhow.
anyhow is an external crate you add to Cargo.toml, same as thiserror:
[dependencies]
anyhow = "1"
Its core is anyhow::Error, a type that can hold any error, and the alias anyhow::Result<T>, which is simply Result<T, anyhow::Error>. Because anyhow::Error converts from any type that implements std::error::Error, the ? operator accepts every error kind you throw at it -- a std::io::Error, a ParseIntError, your own custom type -- with no From impls to write. That is the first big win: in application code you stop defining error enums altogether.
The signature move, though, and the reason people love the crate, is .context(). It wraps an error with a message explaining what you were trying to do when it failed. Raw errors are notoriously unhelpful in isolation -- "No such file or directory" tells you nothing about which file or why you wanted it. .context() fixes that by stacking a readable description on top, while keeping the original error as the cause underneath:
// requires the `anyhow` crate: shown for illustration, not compiled locally
use anyhow::{anyhow, bail, Context, Result};
fn load_config(path: &str) -> Result<String> {
let contents = std::fs::read_to_string(path)
.with_context(|| format!("failed to read config from {path}"))?;
if contents.trim().is_empty() {
bail!("config file {path} is empty");
}
Ok(contents)
}
fn parse_port(s: &str) -> Result<u16> {
let port: u16 = s.parse().context("port must be a valid number")?;
if port < 1024 {
return Err(anyhow!("port {port} is in the reserved range"));
}
Ok(port)
}
fn main() -> Result<()> {
let port = parse_port("8080")?;
println!("using port {port}"); // using port 8080
Ok(())
}
Look at what is absent: no custom error type, no From impls, no map_err. The ? operator swallows a std::io::Error, a ParseIntError, and our own ad-hoc messages alike, and every one of them becomes an anyhow::Error. When load_config fails, the printed report reads like a stack of intentions -- "failed to read config from app.toml", and beneath it the operating system's "No such file or directory". That is the difference between an error you can debug at a glance and one you have to go spelunking for.
Note the two flavours: .context("literal") takes the message eagerly, while .with_context(|| ...) takes a closure that only runs on the error path. Use the closure form whenever building the message costs something (a format! allocation, say), because on the happy path -- which is most of the time -- you do not want to pay for a string you will never show. It is the same eager-versus-lazy distinction we saw with unwrap_or versus unwrap_or_else back in the combinators episodes ;-)
Three macros make ad-hoc errors effortless. anyhow!("message with {value}") builds an anyhow::Error from a formatted string. bail!("...") is pure shorthand for return Err(anyhow!(...)) -- an early-return-with-error in one word. And ensure!(condition, "...") bails unless a condition holds, like assert! but returning an error in stead of panicking, which is exactly what you want when the input is bad but the program should not crash:
// requires the `anyhow` crate: shown for illustration, not compiled locally
use anyhow::{bail, ensure, Result};
fn checked_port(s: &str) -> Result<u16> {
let port: u16 = s.parse()?; // ? converts ParseIntError
ensure!(port >= 1024, "port {port} is reserved"); // bails if the check fails
if port == 0 { bail!("port 0 is never valid"); } // explicit early return
Ok(port)
}
In plain std those macros are just early returns, and writing them out by hand shows precisely what the macros save you -- the whole return Err(...) dance shrinks to a single readable line:
fn parse_port(s: &str) -> Result<u16, String> {
let port: u16 = s.parse().map_err(|_| "not a number".to_string())?;
if port < 1024 {
return Err(format!("port {port} is reserved")); // this is what bail! expands to
}
Ok(port)
}
fn main() {
println!("{:?}", parse_port("8080")); // Ok(8080)
println!("{:?}", parse_port("80")); // Err("port 80 is reserved")
}
These let you write validation as naturally as the happy path, which is the whole point in application code: errors there are reported, not matched. You are not building a type for some downstream caller to inspect -- you are describing, in plain language, what went wrong so a human reading the logs understands it.
To keep anyhow from feeling like sorcery, here is the essence of .context() written in plain std: a small wrapper error that holds a message and a boxed source. This compiles and runs:
use std::error::Error;
use std::fmt;
#[derive(Debug)]
struct Contexted { context: String, source: Box<dyn Error> }
impl fmt::Display for Contexted {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}: {}", self.context, self.source)
}
}
impl Error for Contexted {
fn source(&self) -> Option<&(dyn Error + 'static)> { Some(self.source.as_ref()) }
}
fn parse_port(s: &str) -> Result<u16, Box<dyn Error>> {
s.parse::<u16>().map_err(|e| Box::new(Contexted {
context: String::from("port must be a valid number"),
source: Box::new(e),
}) as Box<dyn Error>)
}
fn main() -> Result<(), Box<dyn Error>> {
let port = parse_port("8080")?;
println!("using port {port}"); // using port 8080
Ok(())
}
That is the entire idea. anyhow::Error is essentially a polished, industrial-strength version of this Box<dyn Error> wrapper: it captures a backtrace (when RUST_BACKTRACE is set), it chains context cleanly so {:#} prints the whole "caused by" ladder, it formats a readable report for you, and -- a neat implementation detail -- it is a single word wide on the stack rather than the two words a Box<dyn Error> fat pointer needs, because it stashes the vtable next to the data on the heap. Same shape you just wrote, better engineered.
You can even build a tiny .context()-style extension trait yourself, which is roughly what anyhow hands you for free. We covered extension traits and blanket impls back in episode 24, so this should look familiar:
use std::fmt;
trait Contextable<T> {
fn context(self, msg: &str) -> Result<T, String>;
}
impl<T, E: fmt::Display> Contextable<T> for Result<T, E> {
fn context(self, msg: &str) -> Result<T, String> {
self.map_err(|e| format!("{msg}: {e}"))
}
}
fn main() {
let r: Result<i32, String> = "x".parse::<i32>().context("parsing the count");
println!("{r:?}"); // Err("parsing the count: invalid digit found in string")
}
That blanket impl<T, E: Display> adds a .context() method to every Result whose error type can be displayed -- which is nearly all of them. The lazy with_context form, which only builds the message on the error path, is a small variation that takes a closure in stead of a &str:
use std::fmt;
trait WithContext<T> {
fn with_context<F: FnOnce() -> String>(self, f: F) -> Result<T, String>;
}
impl<T, E: fmt::Display> WithContext<T> for Result<T, E> {
fn with_context<F: FnOnce() -> String>(self, f: F) -> Result<T, String> {
self.map_err(|e| format!("{}: {e}", f())) // f() runs only on Err
}
}
fn main() {
let path = "cfg.toml";
let r: Result<i32, String> = "x".parse::<i32>().with_context(|| format!("reading {path}"));
println!("{r:?}"); // Err("reading cfg.toml: invalid digit found in string")
}
That is the whole idea anyhow polishes: attach a message, keep the cause, report cleanly. Everything else the crate does -- the backtrace capture, the pretty multi-line report, the downcasting back to a concrete type when you do need it -- is comfort built on top of these two dozen lines.
It is worth stepping back to see why an application error type is a real problem at all, because the languages you may know from earlier in your career solve it very differently.
In Python, every function can raise anything, and the interpreter carries a full traceback for free. Adding context is a matter of raising a new exception from the old one, and the runtime stitches the chain together:
def load_config(path):
try:
with open(path) as f:
return f.read()
except OSError as e:
raise RuntimeError(f"failed to read config from {path}") from e # chains
That from e is Python's .context(), and the traceback is its cause chain -- but it is bookkeeping the interpreter does at runtime, on the heap, for every exception whether you need it or not. Convenient, but not free, and crucially the "type" of what a function might raise is invisible: there is nothing the reader (or compiler) can point at to know what can go wrong.
Go goes the opposite way and makes you wrap by hand, with fmt.Errorf and the %w verb:
func loadConfig(path string) (string, error) {
data, err := os.ReadFile(path)
if err != nil {
return "", fmt.Errorf("failed to read config from %s: %w", path, err) // %w wraps
}
return string(data), nil
}
Go's %w is its .context(), and errors.Is / errors.As walk the chain. It is explicit and cheap, which is nice -- but notice you are typing that if err != nil { return ..., fmt.Errorf(...) } block at every call site. anyhow gives you Go's explicitness (a real value you pass around, cheap, no interpreter magic) with Python's ergonomics (? does the propagation, .context() does the wrapping in one call). You get the good half of each without the tax of either -- which, honestly, is the recurring theme of this whole language ;-)
So when do you pick which? The two crates are complementary, and the rule of thumb is short enough to tattoo on your wrist: use thiserror in your libraries, use anyhow in your binaries.
A library crate exports errors that other people's code will match on. Those callers need a precise, exhaustive enum so they can react differently to a timeout versus a parse failure versus a not-found -- so you spend the effort naming every variant, and thiserror makes that cheap. A binary crate -- your main, your CLI glue, a one-off script -- usually does not care to distinguish failures programmatically. It collects errors from a dozen sources, adds context, and prints a report. Defining a giant enum there would be pure ceremony that nobody ever matches on, so you collapse everything into one flexible anyhow::Error instead.
The common, healthy shape is both at once: a library defines a thiserror enum, and the binary on top consumes it, catches its errors with anyhow, adds context, and returns anyhow::Result<()> from main. In pure std, that same split looks like a library returning a concrete error type and an application unifying everything under Box<dyn Error>:
use std::error::Error;
// library layer: a precise, matchable error
fn lib_parse(s: &str) -> Result<i32, std::num::ParseIntError> {
s.parse()
}
// application layer: unify many error kinds, add context, report
fn app(s: &str) -> Result<i32, Box<dyn Error>> {
let n = lib_parse(s).map_err(|e| format!("in app, parsing '{s}': {e}"))?;
Ok(n * 2)
}
fn main() {
println!("{:?}", app("21")); // Ok(42)
println!("{}", app("x").is_err()); // true
}
The one hard rule to remember: never make your library force anyhow on its users. Returning anyhow::Error from a public library API robs your callers of the ability to match, and it drags your dependency into their build whether they wanted it or not. Precise errors flow outward from libraries; flexible errors are collected inward by applications. Keep that arrow pointing the right way and your error handling scales from a ten-line script to a hundred-crate workspace without ever getting in your way.
anyhow and write a main() -> anyhow::Result<()> that reads a file and parses a number from it, adding a .context() (or .with_context()) at each step. Run it against a missing file and against a file with garbage in it, and read the two reports.bail! and ensure! to validate that a parsed value falls inside a range, then print the error with both {} and {:#} and note how the multi-line "caused by" form differs from the single-line one.thiserror enum, and a binary that consumes it, adds context with anyhow, and returns anyhow::Result<()> from main. Prove to yourself the library never mentions anyhow.That closes the error-handling arc, and the halfway point of the series with it. Bedankt voor het lezen, en tot de volgende keer! ;-)