Custom Error types in Rust and the ? operator

Recently I was "forced" into writing some Rust again, after a few months of working on other things, because I had made a commitment to give a talk on the Rocket framework - a beautiful web framework for Rust.

The slides from the talk can be found here and a more detailed blog post is coming soonish. Meanwhile, the source code can be found here

The problem

One thing I didn't have time to finish in time for the presentation was custom Error handling, my route handlers would simply return diesel::result::Error if anything went wrong with the database request (including trying to query missing records).

#[get("/notes/<id>", format = "application/json")]
fn note_get(db: DB, id: i32) -> Result<JSON<Note>, diesel::result::Error> {  
    let note = get_note(db.conn(), id);
    match note {
        Ok(note) => Ok(JSON(note)),
        Err(err) => Err(err),
    }
}

Since Rocket doesn't understand anything about a Diesel error, other than it being an error, the server will respond with 500 Internal Server Error.

I will be absolutely fine with that for most errors, if the database is down, or if parsing a query failed, but I do want to catch any diesel::result::Error::NotFound errors so that I can return 404.

Rocket's Responder trait

Now, for Rocket to take an Error and use it as a response, the Error needs to implement Rocket's Responder trait.

It's going to look like this in my case:

impl<'r> Responder<'r> for Error {  
    fn respond(self) -> Result<Response<'r>, Status> {
        match self {
            Error::NotFound => Err(Status::NotFound),
            _ => Err(Status::InternalServerError),
        }
    }
}

All we have to do inside the respond function is to add our own logic for taking the error from self and returning a Rust Result error containing the Rocket Status we want.

A custom error type

A logical idea would be to import diesel::result::Error and implement Rocket's Responder trait on it. If that worked we would be done here. 😎

Unfortunately (quick fix wise), Rust will not let you implement traits from one external crates on types from another external crate. Which means we will have to create our own error type, and implement Rocket's Responder trait on this new type.

So let's get to work! We'll start off with this enum:

#[derive(Debug)]
pub enum Error {  
    NotFound,
    InternalServerError,
}

Next, any Rust error needs to implement the Display and Error traits from the standard library.

use std::fmt;  
use std::error::Error as StdError;

impl fmt::Display for Error {  
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match *self {
            Error::NotFound => f.write_str("NotFound"),
            Error::InternalServerError => f.write_str("InternalServerError"),
        }
    }
}

impl StdError for Error {  
    fn description(&self) -> &str {
        match *self {
            Error::NotFound => "Record not found",
            Error::InternalServerError => "Internal server error",
        }
    }
}

This is the minimum we need to implement, a printable representation of the errors for Display, and a description of the errors for Error.

Error also let's you implement an optional cause method that can return a reference to the error that caused your error. Not very useful for this use case.

If we combine that with the Rocket Responder implementation earlier, we could now write our Rocket handler like this:

use diesel::result::Error;  
use error::Error as ApiError;

#[get("/notes/<id>", format = "application/json")]
fn note_get(db: DB, id: i32) -> Result<JSON<Note>, ApiError> {  
    let note = get_note(db.conn(), id);
    match note {
        Ok(note) => Ok(JSON(note)),
        Err(err) => {
            match err {
                Error::NotFound => Err(ApiError::NotFound),
                _ => Err(ApiError::InternalServerError),
            }
        }
    }
}

We call our custom error type ApiError and the Diesel error Error.

We try to fetch a note from the database, and match the result, if we successfully get a note we return it as JSON.

If it is an error, however, we do another match, and map the Diesel Error::NotFound to our own ApiError::NotFound, and any other Diesel errors to ApiError::InternalServerError.

This works, Rocket will now receive our error and process it using the Responder trait. It is a lot of code to write in every handler though.

The From trait

Once upon a time Rust had a trait called FromError, it was specifically designed to handle conversions between different error types.

It has since been generalized into From that can handle conversions between any types.

It works like this:

impl From<DieselError> for Error {  
    fn from(e: DieselError) -> Self {
        match e {
            DieselError::NotFound => Error::NotFound,
            _ => Error::InternalServerError,
        }
    }
}

Basically the same as the error match we just wrote in the route handler. Since we have now moved the conversion logic here we can shorten the route handler to look like this:

#[get("/notes/<id>", format = "application/json")]
fn note_get(db: DB, id: i32) -> Result<JSON<Note>, ApiError> {  
    let note = get_note(db.conn(), id);
    match note {
        Ok(note) => Ok(JSON(note)),
        Err(err) => Err(ApiError::from(err)),
    }
}

ApiError::from(err) is pretty succinct. Still, we can this code even shorter, and easier to read (IMO anyway).

The try! macro

One of the biggest advantages of implementing From is that it would be automatically called by Rust's try! macro.

The try! is essential to error handling in Rust, as it will get the value from a Result, or exits early if the Result contains an error, propagating the error to the caller of the current function (and calling From::from on any errors).

Using the try! macro our route handler can be shortened to this:

#[get("/notes/<id>", format = "application/json")]
fn note_get(db: DB, id: i32) -> Result<JSON<Note>, ApiError> {  
    let note = try!(get_note(db.conn(), id));
    Ok(JSON(note))
}

If the try! fails, the function will exit immediately with an ApiError, so we can be sure that on the line below, we have a note (or more specifically, we can be sure that get_note returned a Result with Ok and not Err).

The ? operator

By adding a ? to something that returns a Result you get the same behavior as if you had used try!.

#[get("/notes/<id>", format = "application/json")]
fn note_get(db: DB, id: i32) -> Result<JSON<Note>, ApiError> {  
    let note = get_note(db.conn(), id)?;
    Ok(JSON(note))
}

This is the final version of this function, and I'm quite happy with how it looks.

More changes to Rust's error handling is proposed in the same RFC that introduced the ? operator.

The full source code for the Rocket based API with error handling applied can be found here.