The Rocket framework framework required endpoint to return values that implement responder. The works similarly for Result<T, E>. Both T and E need to implement responder. But what if you want to use a library like anyhow or eyre to simplify error handling while keeping that idiomatic rust? Using some rust magic this is possible.

The ? operator

For more readable error handling, rust provides the ? operator, which can terminate a function early by aborting in case of an error when the function returns a result. Lets take the following code

#[derive(Debug)]
enum MathError {
    DivisionByZero,
    NonPositiveLogarithm,
    NegativeSquareRoot,
}

fn div(x: f64, y: f64) -> Result<f64, MathError> {
    if y == 0.0 {
        Err(MathError::DivisionByZero)
    } else {
        Ok(x / y)
    }
}

fn div_twice(x: f64, y: f64) -> Result<f64, MathError> {
    let once = div(x, y)?;
    div(once, y)
}

If a zero would be passed to the y parameter in the div_twice function, then the second division would never start in the first place. Under the hood some extra magic happens as well.

If needed, into is called the error, to convert it into the required type. With this in mind, lets start building our error. For this example in adhering to RFC 7807.

Our error type

First lets define a error type. This type be passed around our application. For this example, I’m using the eyre crate, but this would work with the Error type in the anyhow crate as well.

#[derive(Debug)]
pub(crate) struct Error {
    inner: eyre::Report,
    error_type: Option<String>,
    status: Option<Status>,
    title: Option<String>,
    detail: Option<String>,
    instance: Option<String>,
}

impl Error {
    pub(crate) fn new(inner: impl Into<eyre::Report>) -> Self {
        Self {
            inner: inner.into(),
            status: None,
            error_type: None,
            title: None,
            detail: None,
            instance: None,
        }
    }

    pub(crate) fn with_status(self, status: Status) -> Self {
        let mut new = self;
        new.status = Some(status);
        new
    }
}

impl<E: Into<eyre::Report>> From<E> for Error {
    fn from(inner: E) -> Self {
        Self::new(inner)
    }
}

Our error type has the inner field, which points to the error itself. Here we could get the backtrace of other information is needed. The remaining fields is just metadata passed into the response.

A simple constructor is added along with the into method. The into method is quite important here. It allows us to accept any std::error::Error to be cast into our custom error.

Next a Result type:

pub(crate) type Result<T, E = Error> = std::result::Result<T, E>;

Responding to requests

Lastly for our error related code, the code responding to in our endpoints.

#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
struct ErrorResponse {
    #[serde(rename = "type")]
    error_type: String,
    status: u16,
    title: String,
    detail: Option<String>,
    instance: String,
}

impl<'r, 'o: 'r> Responder<'r, 'o> for Error {
    fn respond_to(self, request: &Request<'_>) -> response::Result<'o> {
        let status = self.status.unwrap_or(Status::InternalServerError);

        let error_response = ErrorResponse {
            error_type: self.error_type.unwrap_or_else(|| "about:blank".to_string()),
            status: status.code,
            title: self.title.unwrap_or_else(|| "An unknown error occured".to_string()),
            detail: self.detail,
            instance: self.instance.unwrap_or_else(|| request.uri().to_string()),
        };

        let error_string = serde_json::to_string(&error_response).unwrap();

        Response::build()
            .header(Header::new("Content-Type", "application/json+problem"))
            .status(status)
            .sized_body(error_string.len(), Cursor::new(error_string))
            .ok()
    }
}

First we define ErrorResponse, which is a simple object containing information about what went wrong. It almost directly maps to our Error.

Based on our error we may be able to provide all needed data to the response itself. If that’s not the case, we can have some sane defaults.

Wrapping it into Rocket

After defining all these types, we can use it in a rocket application as so:

#[rocket::get("/error")]
fn error_route() -> Error {
    io::Error::new(ErrorKind::Other, "Something went wrong").into()
}

In this route, we always return an error with the default response. To add a status code to the response, we can use the with_status method defined earlier.

#[rocket::get("/unauthorized")]
fn unauthorized_route() -> Error {
    io::Error::new(ErrorKind::Unauthorized, "You are not allowed to do this")
        .into()
        .with_status(Status::Unauthorized)
}

Lastly, for fallible route, we can return a result or abort early with the ? operator.

#[rocket::get("/fallible")]
fn fallible_route() -> Result<String> {
    let result = some_fallible_function()?;
    Ok(result)
}

If we need to pass a status to the error. We could use the map_err method.

#[rocket::get("/fallible")]
fn fallible_route() -> Result<String> {
    let result = some_fallible_function()
      .map_err(|e| e.into().with_status(Status::BadRequest))?;

    Ok(result)
}

© 2024 Nils de Groot