Adding Context to Rust Errors
Rust doesn’t have exceptions, so we always pass errors by return value. There is a `try!` macro, and an unstable question mark `?` operator, both help with propagating errors easily. But in many cases, we need to add more info to the error making errors easier to debug.
Here is how we usually do it now:
While the code is good enough performance-wise it contains a fairly big amount of punctuation, brackets and repetition. In longer runs of fallible code, it may become inconvenient to write, and hard to read.
But, I feel that it doesn’t have to be like this. We already have the `quick-error` crate which helps with repetitive error handling code. Let’s try to make passing a context easier.
Proposed approach looks like this:
I find it shorter, simpler and easier to read. Note, we also got rid of `and_then` because this way code looks a little bit lighter.
I’ll get to the gory details of the implementation in a moment. To try it you need to checkout from the “context2” branch:
Update: It’s in quick-error 1.1.0 so you can just write:
quick-error = "1.1.0"
Let’s look at the shortcomings of the current implementation:
- Each pair of (context-type, source-error-type) corresponds exactly one enumeration member. I.e. you can’t have two different cases for `io::Error` with `Path`, that named differently (or you can, but only one of them will be accessed through a context)
- Context borrows values with a single lifetime. You can neither have a generic type nor arbitrary lifetimes. But you can have multiple context types translated to a single error case.
The consequences of #1 is very similar to what we have in the `From` trait.
The #2 is a limitation of the current macro system in rust. At least I haven’t any good way to overcome it.
The #1 can be avoided by making a wrapper structures, either around context type itself or around error type. Wrapper structure is just a tuple structure with no traits need to be implemented, so this is a pretty simple hack. More details in this comment on github issue
We have an extension trait `quick_error::ResultExt` that provides a `.context` method:
It just wraps error into `quick_error::Context` structure which is just a tuple:
And final piece of puzzle is an implementation of the `From<Context<..>> for YourError` which is done in quick-error’s style:
As outlined above, multiple `context()` clauses in each case are possible, each converting unique type.
Is the limitation of having only one pair of (context-type, source-error-type) to a single member of enum good enough?
I believe in most cases single source error corresponds to a single enum member, anyway. In case it’s not, you can either use a standard way of `.map_err()`. Or use a wrapper type as described in the comment. Wrapper type is mostly useful when it corresponds to a code structure, i.e. when one function returns one kind of error and another returns different kind of error.
Is the limitation of non-generic types are okay?
Well, I wish we could allow generic types. But for many cases, you can just write two conversions (e.g. from `&Path` and from `&str` instead of `AsRef<Path>`). I think we can also provide a special case for `AsRef<X>`, and maybe few other carefully selected traits (Into?), to cover 99% of cases.
Is the syntax fine?
If you have any strong preference to the function and trait names, argument order, or whatever things please speak up until we have merged it in master.
By the way, syntax plays well with the question mark `?` operator too.
Providing more context to errors is a very valuable thing. By making it easier we encourage good error reporting. By making construct simple we are making code less cluttered and easier to read.