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 optionalcause
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.