Error Handling in Rest APIs using the Axum Framework and Custom Error Model

This article discusses error handling strategies while working with the Axum framework and writing REST APIs in Rust. While Axum is a very well written framework and has nice paradigms, it is our responsibility as programmers to handle errors gracefully and meaningfully within the framework.

We’ll look at two different approaches and build upon these to construct an error handling model that can be used by everyone to share and write high quality handlers with error handling taken care of.

Generic API Error Handling

Axum provides a very simple way of error handling. A principle to understand is that the application cannot crash if an error occurs. All errors must be Infallible .

Simply put, all errors are responses that can be rendered over HTTP.

Just like we would represent a JSON Object as a Response, we can model our errors to be objects that can be served to our clients — with the right response codes. We’ll look two different ways to build this in this post. A simple Status response with String errors. For the sake of simplicity, we will start with text responses here which can be rendered by AXUM’s built in StatusCode error renderer.

Project Setup

Let’s create a new application for this demo.

cargo new axum-error-handling-demo

Add the following dependencies to start writing our APIs.

We’ll add a simple handler that expects a query param and returns the same within our response. It’s like a hello world with an addition of query param value. Below is our app definition that renders this endpoint at /hello

Calling our Basic API

With that setup, we can run our server ( cargo run ) , and test it on our local machine.

Let’s try making a call to our API by using the following endpoint.

http://localhost:3000/hello?name=test

And we would see a response with our name in it.

Now, try calling this API without passing the query param. We see that we have the following response now.

http://localhost:3000/hello

And the response we see is

Failed to deserialize query string: missing field `name`

Why do we see this ? In a proper implementation spec, we should be able to see a validation error message that says that this field is required.

But the above message exposes application implementation details which is not a good practice at all.

Solution

To solve this, we can use a simple approach. We can make our struct to accept an Option<String> , rather than a String Param. That way, if there is no value, we won't get an error message with internal details.

The next step would be to perform a validation on the input to make sure that we can send a custom error response to our clients.

Please find the updated struct below :

Now try calling the same endpoint without the query param. You should see a nice error message that tells you exactly what may be wrong.

http://localhost:3000/hello Required Parameter name is missing within the request.

Json Errors

So far, we’ve seen how to model Errors within Axum framework as simple strings.

Since most REST APIs are written as JSON APIs, let’s see how can we model errors as Json Formatted Response.

In order to model errors as JSON, we need to define our Application Error Models. We’ll create a new ApiError object, which will act as the Root Error. Within this, we will store the http Status code, and add a few helper methods to initialize errors. We're also using a library thiserror that helps us create custom errors easily in Rust.

Translating Errors as Response

As we saw in the first part, an error is just a way to respond to certain failure within the API. It shouldn’t crash our program, but should provide the client with a meaningful status on what the issue might be. When using custom structs, it is our responsibility to translate them into a proper response.

As a framework, axum makes it very simple to deal with errors. You can think of it like just another JSON Response if the call had been successful. Additionally, we would make sure that the Http Response codes are adjusted so that the client can take the necessary action.

To ensure that axum can understand ApiError as the error response type , we will try to implement a trait, IntoResponse for our struct. This tells axum that it's a valid error type. Any payload within this error will be translated as an HttpResponse using Json Serialization.

Below is the implementation for our struct ApiError.

You can see here that we parsed the status code associated with the error, and serialized our struct into a response.

This way, our users now can get a preview of all errors in a nice json formatted manner. Let’s implement a handler and try this out. Notice how the method signature changes for our API.

Testing API

We can repeat our earlier call with a valid query param, and we should see a new response with the right result.

http://localhost:3000/hello?name=test
{ "greeting": "hello", "name": "test" }

Let’s try it out without the query param now.

http://localhost:3000/hello?name=test
{  
    "status_code": 400,  
    "errors": [  
        "Required Parameter name is missing within the request."  
    ]  
}

Conclusion

What you saw in this post is a very simple, yet powerful technique for error handling in Axum framework within Rust. We notice that our handler definitions themselves have simplified and our ApiError struct ensures that it knows how to send a response to the user. It may be cumbersome to set up this project initially, but once setup, all error handling in an application is standardized without any doubt. This ApiError struct can even be separated in a crate and can be used in multiple Axum Projects for error handling.