written on November 06, 2014
Rust just got a huge update: it landed multidispatch and the FromError trait. The combination of both of those things improves error handling in Rust significantly. The change is in fact so significant that you can expect many APIs to change greatly in the upcoming weeks.
While error handling in Rust is definitely not a closed case, I assume that the paradigm enabled through these changes is going to hang around for a long, long time.
This article is going to give a quick introduction in how error handling works from this moment forward and why.
I have written about this problem a bit before but in essence the core issue with error handling is that possible errors become part of your API contract. This means that if you modify the implementation of your function, any new error case you introduce, modifies your contract to your caller ever so slightly. This is especially an issue as you refactor your function to call into a new range of functions that can fail with a whole new set of errors.
The problem is to find a middle ground between extensibility and API stability. As an API developer you want to be able to be future proof in the sense that you can change your implementation and call into entire new libraries that you did not envision in the past. For instance your cache layer of choice might so far only have failed with general IO errors but now you change your implementation to use a new library which all the sudden wants to fail with redis errors. While you could just hide those errors and report them under a new name, it's really more interesting to the user to also expose the original root cause of the error.
And whatever you do, you do not want to negatively affect your existing users.
It's not just evolving APIs though. If you have a function that can fail with two different errors, you want to have to write as little boilerplate as necessary to handle the errors from different origins.
To be a bit more concrete: imagine you build a command line application that does HTTP requests to the internet. Such an application will have to deal with a whole range of failures: command line argument parsing errors, IO errors, HTTP failures, SSL failures etc. Some of those errors you want to be able to act on (for instance to convert HTTP status codes for timeouts from an error into an implicit retry), others you want to bubble up until you eventually show them to the user. And all of that with as little code as possible.
Rust's solution for this problem is the FromError
trait. The idea is
simple but clever. The FromError
trait provides a standardized
conversion of errors into another error. This conversion however for the
most part will not actually "convert" the error but "wrap" it. There is
no requirement for this however, you have all the freedom you need. If
you want you can completely hide the original error. In addition to that
it does not actually restrict it to other errors, so you can convert quite
freely between different things. If you want you can convert error codes
into fully fleshed out error structs through the same machinery.
Errors in Rust are as of now, supposed to implement this trait:
pub trait Error: Send {
fn description(&self) -> &str;
fn detail(&self) -> Option<String> { ... }
fn cause(&self) -> Option<&Error> { ... }
}
This is not a lot of information, and it's quite likely that this trait
will grow in the future, but for the moment it holds at least some
information that we can show error messages. description
returns a
string slice to a general description that is available whereas detail
holds optional information that can provide more details. The latter is
considered to be information that might be required to compute.
The third trait method is cause
which allows an error to point to
another error which was the cause. For instance if you have a library
that does HTTP calls, and one API method fails on the IO layer your error
could point to the originating IoError
as cause.
The actual conversion between errors is kicked off by the FromError
trait which you need to implement for all compatible errors. For instance
for the HTTP library example it could look like this:
use std::{io, error};
enum LibError {
BadStatusCode(int),
IoError(io::IoError),
}
impl FromError<io::IoError> for LibError {
fn from_error(err: io::IoError) -> LibError {
IoError(err)
}
}
In this case we have our own error type LibError
which has two possible
failures: A BadStatusCode
which is a failure that clearly indicates a
problem in our own library as well as IoError
which wraps another error
from another library (in this case the IO system in Rust). Through the
FromError
trait we now get a standardized conversion from IoError
to
LibError
from our own library. This functionality is provided through
multidispatch. All this dispatching however is done at compile time.
At this point we can also to implement the actual error trait to make our error useful. It looks like this:
impl error::Error for LibError {
fn description(&self) -> &str {
match *self {
BadStatusCode => "bad status code",
IoError(err) => "encountered an I/O error",
}
}
fn detail(&self) -> Option<String> {
match *self {
BadStatusCode(code) => Some(format!("status code was {}", code)),
_ => None,
}
}
fn cause(&self) -> Option<&error::Error> {
match *self {
IoError(ref err) => Some(err as &error::Error),
_ => None,
}
}
}
How exactly your error however looks is entirely up to you. For instance
you could convert certain IoError