It's easy to forget how much ceremony Rust error handling used to involve. These days you reach for anyhow in an application and thiserror in a library and the whole thing mostly disappears. It wasn't always like that, and I was reminded this week digging through an old project that predates both.
Before those crates, every binary grew its own bespoke error enum. You'd hand-write a variant per failure mode, then implement Display, then implement std::error::Error, then write a From impl for every foreign error type you wanted the ? operator to swallow. Miss one From and the compiler refused to convert, so ? wouldn't compile, so you wrote another impl.
enum AppError {
Io(std::io::Error),
Parse(std::num::ParseIntError),
}
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) }
}
And that's before Display and Error. Multiply by every error type in a real program and you had pages of mechanical boilerplate that did nothing interesting and which you copied between projects.
There was failure for a while, which helped, and error-chain before that, which I never warmed to. anyhow and thiserror are the pair that finally got the ergonomics right: one for "I just want this to bubble up with context", one for "I'm a library and my callers need to match on the variants". The split is exactly the right one, and the boilerplate above collapses into a derive and a ?. Worth remembering what it replaced.