..

Error handling in rust's rocket web framework

Category: en

I have been working with rust for quite a while now, and I encountered a moment where I need to expose a REST API of the service I am building.

So the endpoint will have multiple responses as below

  • 200 Ok returns data
  • 400 Bad request when request data is invalid
  • 404 Not found when data not found at the service layer
  • 500 Internal server errors for cases like SQL driver failure and so on

Inside the route, it calls a method from the service layer to get data, perform JSON serialization on it, and then respond. In the case of failure, I want to return an error JSON response with an appropriate error message. So the code signature looks like below

#[get("/")]
pub fn get_records(state: &State<AppState>) -> Result<Json<Records>, Error> {
    ...
}

There’s an advantage of doing that, it makes error handling for service calls easier. Instead of having match error handling which is messy in my opinion, I can just use ? operator shortcut in route. Just see the difference between these two below.

pub fn get_records(...) -> Result<Json<Records>, Error> {
    let records = state.service.get_records()?;
    ...
}


pub fn get_records(...) -> Result<Json<Records>, Error> {
    let records = match state.service.get_records() {
        Ok(data) => data,
        Err(..) => ...,
    };
    
    if records.len() == 0 {
        return Err(Error::NotFound(...));
    }
    ...
}

returning record upon success, Json<Records> is simple, it will be 200 Ok response, easy. However, for Error part it can be any of the three (400, 404, 500) I mentioned above.

In rust, it is a little bit tricky when you need to return more than 2 types of responses with different HTTP statuses. Someone asked about this on GitHub before, he wanted to do HTTP redirection, template response, and failure.

As it is also stated by someone in the discussion, it would require something like Either type. Unfortunately, rust does not have it in the standard library. There is an external library but I do not want to go that route.


Enum saves the day

I created an enum, error to handle different errors as below. This error will be used across all layers, presentation, service, and repository. Then a struct is created along with an enum just for JSON serialization later.

// package declarations
...

#[derive(Serialize)]
pub struct ErrorResponse {
    message: String,
}

#[derive(Error, Debug, Clone)]
pub enum Error {
    #[error("{0}")]
    Internal(String),

    #[error("{0}")]
    NotFound(String),

    #[error("{0}")]
    BadRequest(String),
}

Then I will implement one method that will help me convert enum types into rocket HTTP status.

impl Error {
    fn get_http_status(&self) -> Status {
        match self {
            Error::Internal(_) => Status::InternalServerError,
            Error::NotFound(_) => Status::NotFound,
            _ => Status::BadRequest,
        }
    }
}

Now, I just need to use Responder trait on custom Error enum so that I can use it in routes for returning. For reference, you can check out official documentation of the Responder trait implementation.

impl<'r> Responder<'r, 'static> for Error {
    fn respond_to(self, _: &'r Request<'_>) -> Result<'static> {
        // serialize struct into json string
        let err_response = serde_json::to_string(&ErrorResponse{
            message: self.to_string()
        }).unwrap();

        Response::build()
            .status(self.get_http_status())
            .header(ContentType::JSON)
            .sized_body(err_response.len(), Cursor::new(err_response))
            .ok()
    }
}

The code is pretty simple, I use serde_json to serialize the JSON. Then using Response to build the response with different HTTP status and the response body.

If you look into the service layer, it uses the same Error enum for the return result. Same for routes.

use crate::{entities::Record, error::Error};

#[derive(Debug)]
pub struct Service {}

impl Service {
    pub fn new() -> Service {
        Service {}
    }

    pub fn get_records(&self) -> Result<Vec<Record>, Error> {
        let mut records = Vec::new();
        ...
        Ok(records)
    }
    
    ...
}

This may seem a bit complicated compared to languages like Python, Javascript, etc, where returning multiple data-type is much simpler. If you want to see the working example, I created one here.

Once you run the code, go to

  • / for the success response
  • /error for internal server error
  • /bad for HTTP bad request error

PS: there may be a better way of handling an error in the rocket framework, as I am still new to rust programming, what I just showed may not be the best solution. But so far, this is what I find clean and manageable for handling different HTTP errors.
If there’s a better solution, feel free to create an issue in the repository I mentioned above.

🦀