Error handling is the part of Rust where the language's honesty really shows. A function that can fail returns a Result, the type system makes you confront it, and there's no quietly swallowed exception waiting to surprise you in production. That part has always been good. What hasn't always been good is the ergonomics of actually composing those errors, and right now we're in an awkward middle period where the language gives you the tools but not yet the comfort.
the basic problem
Say you've got a function that reads a config file, parses it, and looks something up over the network. Three operations, three completely different error types. The file read gives you a std::io::Error. The parse gives you whatever your parser returns. The network call gives you something else again. And your function has to return a single Result with a single error type. So what goes in the Err?
You can't just bubble them up, because std::io::Error and a parse error are different types, and a function has one return type. The compiler is entirely right to stop you. The question is what you do about it, and the answer in 2017 is: more work than you'd like.
the manual way
The honest, no-dependencies approach is to define your own error enum with a variant for each thing that can go wrong, then teach Rust how to convert each underlying error into your enum by implementing From:
enum AppError {
Io(std::io::Error),
Parse(ParseError),
NotFound(String),
}
impl From<std::io::Error> for AppError {
fn from(e: std::io::Error) -> Self {
AppError::Io(e)
}
}
impl From<ParseError> for AppError {
fn from(e: ParseError) -> Self {
AppError::Parse(e)
}
}
Once those From impls exist, the ? operator does the heavy lifting. When you write let data = read_file(path)?; and read_file returns an io::Error, the ? sees that your function returns AppError, finds the From<io::Error> impl, and converts automatically. That's genuinely lovely, and worth pausing on: the question mark operator only stabilised last year, in 1.13, and before it you wrote try!(...) macros everywhere. Going from try!(try!(foo())) to foo()?? was one of those small changes that quietly improved every program in the language.
The catch is the boilerplate. Every error type you want to absorb needs a variant and a From impl. You also really ought to implement Display and std::error::Error so your enum behaves like a proper error. For a small program that's a dozen lines of tedium. For a large one it's a chore you repeat in every module, and it's exactly the kind of mechanical code you resent writing by hand.
the crate that helps, for now
The current answer to that tedium is error-chain. It's a macro that generates the enum, the From impls, the Display, the Error trait, and a backtrace-carrying error type, all from a fairly compact declaration:
error_chain! {
foreign_links {
Io(std::io::Error);
Parse(::std::num::ParseIntError);
}
errors {
NotFound(key: String) {
description("key not found")
display("key not found: {}", key)
}
}
}
That expands into roughly what I was writing by hand, plus a chain_err method that lets you attach context as an error travels up the stack, so instead of a bare "file not found" you get "failed to load config: file not found". Context is the thing manual enums make awkward and it's the thing that actually helps when you're staring at a failure at three in the morning.
I have mixed feelings about error-chain, if I'm honest. The macro is a lot of magic, the generated types can be confusing when the compiler points at code you never wrote, and the backtrace support is fiddly. But it removes the worst of the boilerplate, and for application code, the kind where you mostly want to say "this went wrong, here's some context, propagate it", that's a fair trade.
library versus application
The distinction that matters, and the one I wish I'd understood sooner, is between library code and application code.
In a library, your errors are part of your public interface. Callers will want to match on them, handle some cases and not others, and they don't want your error type dragging in your choice of helper crate. So libraries should expose a clean, deliberate error enum, implement the standard Error trait, and avoid imposing a dependency on anyone who uses them. The manual approach, tedious as it is, is the right one here.
In an application, you mostly just want to fail with a useful message and a trail of context, and you rarely need to match exhaustively on every variant. That's where a helper like error-chain pulls its weight, because the boilerplate buys you nothing and the context buys you a lot.
where this is heading
What strikes me is that the language already has the right foundation. Result, the ? operator, the From conversions, the Error trait: the bones are all there and they're good. What's missing is a comfortable, blessed way to use them without ceremony, and right now we're papering over that gap with macros that do a bit too much.
I'd put money on this getting better. The ingredients are sitting right there, and the community clearly wants something lighter than error-chain for the common case. For now, the rule I follow is simple: write the error enum by hand in libraries, reach for a helper in applications, and be grateful every single day for the ? operator, which turned the worst part of this into one well-chosen character.