Ramblings of an aging IT geek
← Ramblings of an aging IT geek
rust

error handling before anyhow existed

A look back at the boxed-error and failure-crate years of Rust error handling, and why anyhow and thiserror feel like such a relief now.

Code on a screen

Rust error handling is in a genuinely good place now, and it's worth remembering how recently it wasn't, because the relief is the whole point. The pattern most of us reach for today is thiserror for libraries and anyhow for applications, and the ? operator stitches it all together so naturally that newcomers must assume it was always this pleasant. It wasn't.

Cast your mind back a few years. ? itself only landed in 2016, replacing the try! macro, and even with ? you still had the question of what type sat on the error side of your Result. If two functions returned different error types, ? wouldn't help you, because it needs a From impl to convert one into the other, and writing those by hand for every pair of errors in a real program was tedious enough that people just didn't bother.

So the common escape hatch was a boxed trait object:

fn run() -> Result<(), Box<dyn std::error::Error>> {
    let config = std::fs::read_to_string("config.toml")?;
    let parsed: Config = toml::from_str(&config)?;
    do_the_thing(parsed)?;
    Ok(())
}

That works, and ? converts each concrete error into the box for you, which is lovely. But you've thrown away the type, you get no context about where it happened, and a Box<dyn Error> is awkward to send across threads without remembering to add the + Send + Sync incantation. It got you moving but it wasn't a comfortable place to live.

A close-up of source code

The crate that filled the gap for a while was failure, from the language team's orbit, and a lot of code from 2018 and 2019 still carries it. It gave you a Fail trait, a Error type that captured a backtrace, and a derive macro so you didn't hand-write boilerplate. It was a real step up. It also never quite reconciled itself with the standard library's std::error::Error trait, and that friction is ultimately why the community has been quietly moving on from it this year.

What anyhow and thiserror got right, both from David Tolnay and both now the de facto answer, is the split. thiserror is for when you're writing a library and you want a real, matchable, enumerated error type that your callers can reason about, with the From derives generated for you. anyhow is for when you're writing an application and you genuinely don't care to enumerate every failure: you just want one type that swallows anything, carries a backtrace, and lets you bolt on context with .context("reading config") so the eventual message tells a story rather than just naming the leaf cause.

The reason this division feels so right is that it matches the actual question you're asking in each place. A library doesn't know how its errors will be handled, so it should hand back something precise and let the caller decide. An application is the caller, it's the end of the line, and most of the time the only sensible thing it does with an error is log it and exit, so a rich opaque type with good context is exactly what it wants.

I still hit old failure code now and again, and converting it over is usually a pleasant afternoon rather than a slog. But the thing I'd want a Rust programmer arriving today to understand is that the ergonomics they're enjoying were hard-won. Every time ? quietly converts an error and a .context() call turns a baffling leaf message into something you can act on, that's a problem someone spent a real amount of effort making invisible.