What should you get from an Error?

Jonathan Reem
3 min readAug 20, 2014

--

When crafting a general error type for Iron I came to a conclusion about the two different roles an error can serve: errors can be crafted for reporting, or they can be crafted for handling.

Iron has rather demanding needs from an error type — it needs to be extensible not only within Iron itself and its modules, but across library boundaries, and more importantly is has to be possible to write generic handlers that can handle those errors across library boundaries.

One way to solve this problem is to simply ignore the second requirement, that errors be handle-able, and stick with something optimized for reporting. Right now this looks like:

fn error_producer<T>() -> Result<T, Box<Show>>

This is very limiting — the only thing we can do with a Box<Show> error type is show it. That gives us basic reporting capabilities, but since it’s impossible to do anything else with this error we might as well just forgo error handling entirely and just print all errors as soon as they happen.

We can do a little better with a specific error trait to give us a bit more information:

pub trait Error {
fn kind(&self) -> &'static str;
fn description(&self) -> Option<String>;
fn cause(&self) -> Box<Error>;
}

fn error_producer<T>() -> Result<T, Box<Error>>

Now we can actually do a tiny bit of work, we can print the kind of the error and its description separately, but more importantly we can track a stack of errors if we need to, which can be very helpful in disambiguating exactly where something started to go wrong.

But, there’s still a major problem with this. The only way we can handle an error with this scheme is by matching the string returned by Error::kind, which is error-prone and extremely sub-optimal for a language with a type system as powerful as Rust’s. We can do better.

One way around this to use some phantom types:

pub trait Error<Mark: 'static, Cause> { 
fn name(&self) -> &'static str;
fn description(&self) -> Option<String>;
fn cause(&self) -> Cause;
fn is<Other: 'static>(&self) -> bool {
TypeId::of::<Other>() == TypeId::of::<Mark>()
}
}

This Error trait makes use of a phantom type — Mark— to tell us what kind of Error it is in a more type-safe way than a string could. Now we can at least attempt to handle errors by checking if they are an error we can handle using is and anyone can implement a new Error with a new Mark, so this remains extensible and movable across abstraction layers.

There’s one remaining problem with this scheme — let’s say I catch an Error<ParseError, Original> and now know I have a Parse Error and can handle it. There remains no way for me to get back to the original Parse Error representation. We’re back to our original goal: handling errors across library boundaries.

My original rust-error implementation dealt with this issue rather inelegantly, but I think that the solution here is a much better one — we can solve this by shifting around the way we represent Mark:

pub trait Error: 'static {
fn name(&self) -> &'static str;
fn description(&self) -> Option<String>;
fn is<O: 'static>(&self) -> bool {
(self as &Any).is::<O>()
}
fn cause(&self) -> Option<Box<Error>>;
fn downcast<O: 'static>(&self) -> Option<&O> {
(self as &Any).downcast_ref::<O>()
}
}

Currently ‘static bounds on traits don’t work, but theoretically this would allow us to have safe downcasting to errors that we can actually work with, by allowing access to the actual error. This gives us runtime checking and handling of errors in an extensible way that allows us to, as our original goal states, handle errors across library boundaries.

I’ve implemented a working version of this proposal that compiles with todays rust nightly and doesn’t rely on ‘static bounds here. It exposes an API very similar to this one, but is slightly modified to work with the current state of rustc (specifically the lack of DST).

I propose that this error representation is used in Iron and, maybe, as Rust’s universal error type to enhance error interoperability throughout both the standard library and in third party libraries.

Discuss on Reddit.

Discuss on Hacker News.

--

--

Jonathan Reem

Software Engineer by day, looking for work in Rust at night