I was tidying an old project this week and found a module that does nothing but convert one error type into another. Forty lines of impl From<X> for MyError. It took me a moment to remember why I ever wrote such a thing, and then it came back: this is what we all did before anyhow existed.
The problem was real. ? only works if the error on the left can be turned into the error your function returns. Every crate had its own error enum (io::Error, serde_json::Error, some database crate's bespoke thing), and your function returned MyError, so you had to teach the compiler how to bridge each one. That meant a sprawling enum and a From impl per variant, by hand, for every dependency that could fail.
enum MyError {
Io(std::io::Error),
Parse(std::num::ParseIntError),
}
impl From<std::io::Error> for MyError {
fn from(e: std::io::Error) -> Self { MyError::Io(e) }
}
The lazier alternative was Box<dyn std::error::Error>, which swallowed anything but threw away the type and read like an admission of defeat. So we wrote the boilerplate, or reached for error-chain, which automated it with a macro that I never fully trusted and never fully understood.
What anyhow and thiserror did, splitting "I am an application and just want a context-rich error to bubble up" from "I am a library and want a real typed error", feels obvious now. It wasn't obvious then. It's worth remembering, when a crate makes a whole category of tedium vanish, that someone had to first feel the tedium clearly enough to name it.