The single nicest thing about Rust's ? operator is also the thing that trips everyone up the first time. It only works if the error type of the thing you're calling can be turned into the error type your function returns. The moment your function touches two different libraries, an IO error from the standard library and, say, a parse error from somewhere else, ? stops compiling and you're left deciding how to unify them. There is no obvious right answer in late 2017, and I keep relitigating it on every small tool I write.
The options, as I see them today, are three.
You can write one big error enum for your program, with a variant per underlying error and a From implementation for each so ? converts automatically. This is the "correct" answer in the sense that callers can match on the variant and behave differently. It's also a lot of typing for a tool that's never going to match on the variant, because the only consumer is a human reading a log line.
enum AppError {
Io(io::Error),
Parse(chrono::ParseError),
Http(reqwest::Error),
}
impl From<io::Error> for AppError {
fn from(e: io::Error) -> Self { AppError::Io(e) }
}
// ...and the same dance again for the other two
You can box it: have everything return Result<T, Box<dyn Error>>. Now ? works across libraries with no From impls to write, because the standard library already converts most error types into a boxed trait object for you. The cost is that you've thrown away the ability to match on what went wrong. For a single-shot CLI that does its job and exits with a message, that's usually a cost worth paying, and it's where I land most often.
fn run() -> Result<(), Box<dyn Error>> {
let data = fs::read_to_string(path)?; // io::Error
let when = NaiveDate::parse_from_str(&data, "%Y-%m-%d")?; // ParseError
upload(when)?; // reqwest::Error
Ok(())
}
Or you can reach for error-chain, which generates the enum and the From impls and a backtrace-ish chain of causes from a macro. It's clever and a lot of people swear by it, but the macro produces types that are hard to reason about when something goes wrong inside it, and the error messages when you misuse the macro are genuinely unpleasant. I've used it and bounced off it more than once.
What I actually want is the boxed approach but with a way to attach context as the error bubbles up, so a bare "No such file or directory" becomes "while reading the config: No such file or directory". Today that means writing a little wrapper by hand, a struct that holds a message and a boxed source, and an extension trait with a .context() method. It's twenty lines I copy between projects, which is a clear sign the ecosystem has a gap here. Someone will eventually write the crate that does exactly this and we'll all delete our hand-rolled versions overnight. For now, the boxed dyn Error plus a homemade .context() is the least-bad option for the small tools I write, and I've stopped feeling guilty about not building the seven-variant enum every time.