How to Handle Errors in Axum: Practical Guide for Developers

When building web applications, error handling is essential to ensure that users are provided with helpful feedback and that the server can gracefully handle unexpected issues. In this guide, we’ll explore how to handle errors effectively using Axum, a powerful Rust web framework.

By the end of this tutorial, you will understand how to manage errors efficiently and feel confident about adding proper error handling to your Axum applications.


Step 1: Setting Up Axum

First, we need to set up a minimal Axum application. In your Cargo.toml, specify the required dependencies:

[dependencies]
axum = "0.6"
tokio = { version = "1", features = ["full"] }

Then, create a simple "Hello, World!" application. We will build on this example and add error handling as we go.

use axum::{
    routing::get,
    Router,
};

#[tokio::main]
async fn main() {
    let app = Router::new().route("/", get(root));

    axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

async fn root() -> &'static str {
    "Hello, World!"
}

This is a basic server that responds with “Hello, World!” when you visit the root route. Now that we have the foundation, let’s add some error handling.


Step 2: Introducing Result-Based Error Handling

Axum uses Rust’s Result type to handle errors. This pattern is common in Rust and works well in Axum for returning errors from request handlers. Let’s modify our root function to simulate an error by returning a Result.

use axum::{
    response::IntoResponse,
    http::StatusCode,
    routing::get,
    Router,
};

#[tokio::main]
async fn main() {
    let app = Router::new().route("/", get(root));

    axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

async fn root() -> Result<&'static str, AppError> {
    Err(AppError::NotFound)
}

#[derive(Debug)]
enum AppError {
    NotFound,
}

impl IntoResponse for AppError {
    fn into_response(self) -> axum::response::Response {
        let (status, body) = match self {
            AppError::NotFound => (StatusCode::NOT_FOUND, "Not Found".to_string()),
        };
        (status, body).into_response()
    }
}

Here’s what’s happening:

  • The root function now returns a Result<&'static str, AppError>, where AppError is an enum that represents various error states.
  • We simulate an error by returning Err(AppError::NotFound). This allows us to respond with a 404 status and a message like “Not Found”.

This pattern is helpful because it allows you to handle different kinds of errors and return appropriate HTTP status codes and responses.


Step 3: Handling Multiple Errors

Next, let’s add more error types to handle a variety of cases. We’ll introduce InternalServerError and extend our AppError enum to cover more scenarios.

use axum::{
    response::IntoResponse,
    http::StatusCode,
    routing::get,
    Router,
};

#[tokio::main]
async fn main() {
    let app = Router::new().route("/", get(root));

    axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

async fn root() -> Result<&'static str, AppError> {
    Err(AppError::InternalServerError)
}

#[derive(Debug)]
enum AppError {
    NotFound,
    InternalServerError,
}

impl IntoResponse for AppError {
    fn into_response(self) -> axum::response::Response {
        let (status, body) = match self {
            AppError::NotFound => (StatusCode::NOT_FOUND, "Not Found".to_string()),
            AppError::InternalServerError => (StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error".to_string()),
        };
        (status, body).into_response()
    }
}

Key Concepts:

  • Error Variants: We’ve extended AppError to include both NotFound and InternalServerError variants. This allows us to handle different error conditions.
  • Matching Errors to Responses: In the IntoResponse implementation for AppError, we match each error variant to an appropriate HTTP status code and message.

Now, the application can respond with either a 404 or a 500 status code, depending on the error we simulate.


Step 4: Using Axum’s Built-In Error Trait

Axum provides some built-in support for error handling, allowing us to streamline the process. In this step, we’ll implement the fmt::Display trait for our error enum to improve error messages.

use axum::{
    response::{IntoResponse, Response},
    http::StatusCode,
    routing::get,
    Router,
};
use std::fmt;

#[tokio::main]
async fn main() {
    let app = Router::new().route("/", get(root));

    axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

async fn root() -> Result<&'static str, AppError> {
    Err(AppError::InternalServerError)
}

#[derive(Debug)]
enum AppError {
    NotFound,
    InternalServerError,
}

impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{:?}", self)
    }
}

impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        let (status, body) = match self {
            AppError::NotFound => (StatusCode::NOT_FOUND, "Not Found".to_string()),
            AppError::InternalServerError => (StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error".to_string()),
        };
        (status, body).into_response()
    }
}

Concept Highlight:

  • Error Display: The fmt::Display trait implementation gives a better format for error messages, which can be helpful for debugging and logging.
  • Axum leverages this to provide structured error handling.

With this approach, we can capture error messages in a more readable format, making it easier to debug.


Step 5: Challenge – Add a New Error Type

Now it’s your turn! Add a new error type called BadRequest. Follow these steps:

  1. Add a new variant to the AppError enum.
  2. Update the IntoResponse trait implementation to handle the new error.
  3. Modify the root function to simulate this new error.

Recap and Conclusion

In this tutorial, we’ve covered how to handle errors in Axum:

  • Setting Up: We began with a basic Axum app and simulated an error.
  • Error Types: We learned how to use the Result type for error handling, creating an AppError enum to represent different error conditions.
  • Multiple Errors: We expanded our error handling to handle different status codes, like NotFound and InternalServerError.
  • Built-In Support: We enhanced error handling by implementing the fmt::Display trait to improve error messages.

With these techniques, you can implement robust error handling in your Axum applications, leading to better user experiences and more maintainable code. For further learning, check out Axum’s official documentation and explore more advanced features.

Happy coding!