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

How We Did Rust Error Handling Before anyhow Saved Us All

A look back at hand-rolled error enums, the From-impl boilerplate, and the crates that eventually fixed it.

Rust source on a screen

Junior engineers reach for anyhow::Result and ? and never think about it, which is exactly as it should be. But I keep a small grudge-shaped fondness for the era before it, because the pain is why the good crates exist, and it's worth remembering what they actually solved.

In the early days, before ? was even stable, you had try!. And before anyhow and thiserror were the obvious answer, propagating errors across module boundaries in a real application meant hand-rolling an error enum and then writing the conversions by hand.

enum AppError {
    Io(std::io::Error),
    Parse(std::num::ParseIntError),
    Db(String),
}

impl From<std::io::Error> for AppError {
    fn from(e: std::io::Error) -> Self { AppError::Io(e) }
}

impl From<std::num::ParseIntError> for AppError {
    fn from(e: std::num::ParseIntError) -> Self { AppError::Parse(e) }
}

Multiply that From block by every error type any dependency might hand you, and add a Display impl by hand, and a std::error::Error impl, and you start to understand why people wrote macros, badly, to generate it.

The crates that arrived

error-chain came first and tried to fix it with a big macro that built the enum, the conversions, and a backtrace story for you. It worked, and it was a lot of magic in a error_chain! { } block that you either trusted completely or fought with at three in the morning. It had the right instinct: nobody should write From impls by hand. It just bought the convenience with a lot of opaque generated code.

Then the split that stuck. failure had a go at it, and thiserror and anyhow are where most of us landed. The insight was separating the two jobs we'd been mashing together. When you're writing a library, your callers want to match on your errors, so you want a real typed enum, and thiserror derives all the Display and From and Error boilerplate from attributes. When you're writing an application, you mostly want to attach some context and bubble the thing up to a top-level handler that prints it and exits non-zero, and anyhow gives you exactly that with .context("loading config").

let cfg = fs::read_to_string(path)
    .context("reading config file")?;

That .context() call is the whole game. The thing I spent years not having was a cheap way to say "and here's what I was trying to do when it broke", attached to an error without inventing a new variant for it.

Why bother remembering

Two reasons. First, the library-versus-application distinction is still the right call, and people who started after anyhow existed sometimes reach for it inside a library and hand their callers an opaque blob they can't match on. Knowing why the split exists stops you doing that.

Second, it's a clean example of the ecosystem doing its job. Nobody designed this from the top. People felt the same boilerplate pain, tried several shapes, and the good one won by being less clever than the alternatives. The current setup is genuinely lovely to use. It just didn't arrive that way, and the hand-written From impls are the fossil record that proves it.