Custom Error types that actually work!

TL;DR: Github repo. Are you tired of writing the same verbose error handling boilerplate in your Axum handler? Me too! 🙄By making a ritual AppError new type that wraps anyhow::Error and implementation IntoResponse , FromYou Can Ditch All Those Ugly Matching Statements and Adopt Beautiful ? Operator. Your handler functions go from dirty error-matching shenanigans to clean, readable code that automatically converts any errors into proper HTTP responses. It’s like magic, but with more crabs!


Lately I’ve been doing a lot of digging into Exum crates for one of my Rust web projects. There are a lot of options out there for Rust web applications, but it looks like we’ve settled on Axum As in go in the crate. Before you start reading this, if you haven’t checked it out yet – do this now …I will wait.

Okay, you’re back! love it? Yes! 🥳 Now, let’s talk about the learning project I’m working on. Ever since I discovered htmx I’ve been immersing myself in the world of web development with HATEOAS as my priority. The simplicity of using hypermedia is what excites me, and helps me get through all the JavaScript tedium. I suggest you go and read Hypermedia Systems, which I have been doing for the past few weeks.

So, in keeping with the spirit of this book, I was building some hypermedia driven system. But instead of using Python and Flask as the book says, I’ve opted to put on my crab hat and do it in Rust. I’m like a big boy. In this post I won’t explain how I did this (link to a future post will be here), but rather how I used Rust’s amazing features to eliminate a lot of boilerplate code on my end.

errors errors

To be a good web builder, you’ll want to make sure that you return proper HTTP status codes. (I’m looking at you 200 OK with an error message). So in my handler functions I make sure to explicitly return the status code as part of my return tuple. Something like this:

Ok((StatusCode::OK, Html("")))
// Or an Error:
Err((StatusCode::INTERNAL_SERVER_ERROR, format!("Error processing hello world")))

This will signal to the user (and Axum) that the request was either 200 OKAnd here is the HTML, or 500 Internal Server Error And an angry string. Nifty!

With the glory of Rust’s Result enum, we are ready to handle any errors that may happen to us. So, I just match on a call that may fail, and return something based on that ResultIn practice, say in this handler function that finds a Contact According to its ID in the database, it will look something like this:

#[axum::debug_handler]
async fn get_edit_contact(
    State(state): State<AppState>,
    Path(id): Path<i64>,
    // The function signature tells us what we expect back
) -> Result<(StatusCode, Html<String>), (StatusCode, String)> {
 
    // This call can fail so let's match its Result
    let contact = match Contact::find_by_id(&state.db, id).await {
        // We're good return back the `Contact` into the `contact` variable
        Ok(contact) => contact, 
        // We're NOT good, return the tuple with a status code of 500
        Err(e) => {
            return Err(
                (
                StatusCode::INTERNAL_SERVER_ERROR,
                format!("Failed to find contact: {e}")
                )
            )
        }
    };
 
    let edit_template = EditContactTemplate { contact };
 
    // This can also fail, but we dont need to store it into a variable,
    // we just need to return.
    match edit_template.render() {
        // Looks good, return the HTML and 200 OK
        Ok(html) => Ok((StatusCode::OK, Html(html))),
        // Again 500 Bad, be angry here
        Err(e) => {
            Err(
                (
                StatusCode::INTERNAL_SERVER_ERROR,
                format!("Failed to render template: {e}")
                )
            )
        }
    }
}

He Very of boilerplate code. While I enjoy the verbosity of Rust (makes me feel completely safe and comfortable), it gets old really fast. Especially when you have multiple handler functions, which invoke multiple separate calls that may fail. Let’s bring another amazing feature of Jung new type of patternAnd keep it simple 👏

Creating your own error type

I won’t go too much into the new types, as there is an excellent guide that I encourage you all to read. Simply put, they are thin wrappers that allow you to extend the functionality of existing types that are not native to your crate. And I’m going to use it to enhance the implementation IntoResponse characteristic in a type I have dubbed AppErrorAnd then allow it (whatever the error is) to be converted to ::error somehow,

Let’s first create a new type of this wrapper:

pub struct AppError(anyhow::Error);

here i am wrapping anyhow::Error in a new type called AppErrorI can do the same for any other type, and simply create a wrapper around it (ie a Vec Covering: struct Wrapper(Vec)Now comes the fun part, implementing certain properties.

to implement IntoResponse attribute in this new type from Axum, we only need to implement it into_response Function, which needs to be returned Response Type. Let’s look at some code:

impl IntoResponse for AppError { // 1
    fn into_response(self) -> Response { // 2
        (StatusCode::INTERNAL_SERVER_ERROR, self.0.to_string()).into_response() // 3 
    }
}

And that’s exactly how we’ve implemented our way of returning an error response from the handler function. Let me explain the code a bit:

  1. implementation block for IntoResponse For AppError
  2. This is the only task that requires IntoResponse And we are just returning Response from axum crete
  3. we just return a tuple of one StatusCode and a String coming from element 0 AppError structure. Oh, and convert all that into one Response

here’s something else complicated version of the same code, it uses a templating engine to return some nicely formatted web pages. This version simply extends the above, but should demonstrate that it all works really well with the rest of your codebase.

impl IntoResponse for AppError {
    fn into_response(self) -> axum::response::Response {
        // Returning a HTML page for an error
        let template = Error5xxTemplate { // 1
            // Select element 0, as that is our anyhow::Error string and convert it so it works with our template
            error: self.0.to_string(),
        };
        match template.render() { // 2
            // If the template render is successful, return the HTML page with the error
            Ok(html) => (StatusCode::INTERNAL_SERVER_ERROR, Html(html)).into_response(),
            // The render has failed catastrophically - just return some string
            Err(_) => (StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error").into_response(),
        }
    }
}

Well, there’s a lot more going on here. Most of it is the same as before, but let me summarize it:

  1. Somewhere in my code base I have Error5xxTemplate The structure I use with Askama Templating Crate
  2. I make sure the template renders properly, if so – return a 5xx error page, if not I just give a 500 Error and return string.

absolutely gorgeous error page

Now that we have a IntoResponse Was implemented. let’s give ours AppError Ability to pick up errors from anywhere.

Converting other error types

to make AppError A little more flexible, I wanted to be able to automatically convert any* (*I’ll come back to this in a moment) error type. Let’s look at some code and understand what it means:

impl<E> From<E> for AppError // 1
where
    E: Into::Error> // 2 
{
    fn from(err: E) -> Self { // 3
        Self(err.into())
    }
}

we are taking advantage of it Very Mighty crate here, anyhow. Which allows us to work more efficiently with errors in Rust. In our case we are using its popularity and the ability of other crates to convert to this error type.

Let me explain this line by line:

  1. we are implementing From For AppErrorusing generic types E We can do comprehensive implementation. This will be equal to impl From<:error> for AppErrorthat converts sqlx::Error type in AppError
  2. this is significant bitAnd why it actually limits us to certain errors. after getting it where section, we only allow generic types E Be the people who already support it Into characterized by anyhow::ErrorBasically we are limited to types that already support conversion to this error type,
  3. We just need to implement it from Function that takes the error and returns itself. Using the support mentioned above, we can simply take the error and run .into() Conversion.

Using this common attribute approach we are essentially creating the following Rust code:

// SQLX:
impl From::Error> for AppError {
    fn from(err: sqlx::Error) -> Self {
        Self(err.into()) // sqlx::Error -> anyhow::Error -> AppError
    }
}
// serde_json
impl From::Error> for AppError {
    fn from(err: serde_json::Error) -> Self {
        Self(err.into()) // serde_json::Error -> anyhow::Error -> AppError
    }
}
// reqwest
impl From::Error> for AppError {
    fn from(err: reqwest::Error) -> Self {
        Self(err.into()) // reqwest::Error -> anyhow::Error -> AppError
    }
}
// ...

And just like that… you get the picture!

error conversion

actual implementation

ok, so we have our new type AppErrorIt contains everything we need to convert our error type. How does it actually work? Well, let’s go back to ourselves get_edit_contact Handler function from before, and see what’s changed:

#[axum::debug_handler]
async fn get_edit_contact(
    State(state): State<AppState>,
    Path(id): Path<i64>,
) -> Result<(StatusCode, Html<String>), AppError> {
    let contact = Contact::find_by_id(&state.db, id).await?; // sqlx::Error -> AppError
    let edit_template = EditContactTemplate { contact };
    let html = edit_template.render()?; // askama::Error -> AppError
    Ok((StatusCode::OK, Html(html)))
}

Wow, it’s tighter than ever. yes we are using it ? operator we propagate errors up the call stackmeans the value of Result returned by both Contact::find_by_id And .render() are returned back to Axum as AppError new types

This means that we no longer have to deal with error handling within the function itself, and we are simply returning the same error type. Since it’s the same error type, both the function handler and Axum are happy to receive it! 🥳Huzzah!

If you want to see the entire codebase in action, you can check out my GitHub repo here. And please don’t mind the mess, this is just a learning repo!



Leave a Comment