Skip to main content
Axum provides multiple ways to handle errors in your application. Understanding these patterns will help you build robust web services.

Basic error handling

The simplest way to handle errors is using Result types:
use axum::{
    Router,
    routing::get,
    http::StatusCode,
};

async fn handler() -> Result<String, StatusCode> {
    if something_went_wrong() {
        return Err(StatusCode::INTERNAL_SERVER_ERROR);
    }
    Ok("Success".to_string())
}

fn something_went_wrong() -> bool {
    false
}

let app = Router::new()
    .route("/", get(handler));
Both Ok and Err variants must implement IntoResponse.

Custom error types

Create application-specific error types that implement IntoResponse:
use axum::{
    response::{IntoResponse, Response},
    http::StatusCode,
    Json,
};
use serde::Serialize;

#[derive(Debug)]
enum AppError {
    NotFound,
    Unauthorized,
    DatabaseError(String),
    ValidationError(String),
}

impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        #[derive(Serialize)]
        struct ErrorResponse {
            message: String,
        }

        let (status, message) = match self {
            AppError::NotFound => (
                StatusCode::NOT_FOUND,
                "Resource not found".to_string(),
            ),
            AppError::Unauthorized => (
                StatusCode::UNAUTHORIZED,
                "Unauthorized access".to_string(),
            ),
            AppError::DatabaseError(msg) => (
                StatusCode::INTERNAL_SERVER_ERROR,
                format!("Database error: {}", msg),
            ),
            AppError::ValidationError(msg) => (
                StatusCode::BAD_REQUEST,
                msg,
            ),
        };

        (status, Json(ErrorResponse { message })).into_response()
    }
}

// Use in handlers
async fn get_user() -> Result<String, AppError> {
    Err(AppError::NotFound)
}

Using the ? operator

Convert errors automatically with From implementations:
use axum::{
    response::{IntoResponse, Response},
    http::StatusCode,
};

#[derive(Debug)]
enum AppError {
    Database(DatabaseError),
    Validation(ValidationError),
}

#[derive(Debug)]
struct DatabaseError;

#[derive(Debug)]
struct ValidationError;

// Implement From to use ? operator
impl From<DatabaseError> for AppError {
    fn from(err: DatabaseError) -> Self {
        AppError::Database(err)
    }
}

impl From<ValidationError> for AppError {
    fn from(err: ValidationError) -> Self {
        AppError::Validation(err)
    }
}

impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        let status = match self {
            AppError::Database(_) => StatusCode::INTERNAL_SERVER_ERROR,
            AppError::Validation(_) => StatusCode::BAD_REQUEST,
        };
        (status, format!("{:?}", self)).into_response()
    }
}

// Now you can use ? operator
async fn handler() -> Result<String, AppError> {
    let data = query_database()?;
    validate_data(&data)?;
    Ok(data)
}

fn query_database() -> Result<String, DatabaseError> {
    Ok("data".to_string())
}

fn validate_data(_data: &str) -> Result<(), ValidationError> {
    Ok(())
}

Real-world error handling

A complete example with proper error handling:
use axum::{
    Router,
    routing::post,
    extract::{State, Json, rejection::JsonRejection},
    response::{IntoResponse, Response},
    http::StatusCode,
};
use serde::{Deserialize, Serialize};
use std::sync::Arc;

// Application errors
#[derive(Debug)]
enum AppError {
    JsonRejection(JsonRejection),
    DatabaseError(String),
}

impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        #[derive(Serialize)]
        struct ErrorResponse {
            message: String,
        }

        let (status, message) = match &self {
            AppError::JsonRejection(rejection) => (
                rejection.status(),
                rejection.body_text(),
            ),
            AppError::DatabaseError(_) => (
                StatusCode::INTERNAL_SERVER_ERROR,
                "Database error occurred".to_string(),
            ),
        };

        (status, Json(ErrorResponse { message })).into_response()
    }
}

impl From<JsonRejection> for AppError {
    fn from(rejection: JsonRejection) -> Self {
        Self::JsonRejection(rejection)
    }
}

// Custom JSON extractor with our error type
use axum::extract::FromRequest;

#[derive(FromRequest)]
#[from_request(via(axum::Json), rejection(AppError))]
struct AppJson<T>(T);

impl<T> IntoResponse for AppJson<T>
where
    axum::Json<T>: IntoResponse,
{
    fn into_response(self) -> Response {
        axum::Json(self.0).into_response()
    }
}

// Application state
#[derive(Clone)]
struct AppState {
    db: Arc<Database>,
}

struct Database;

impl Database {
    async fn save_user(&self, _user: &CreateUser) -> Result<User, String> {
        Ok(User { id: 1, name: "Alice".to_string() })
    }
}

#[derive(Deserialize)]
struct CreateUser {
    name: String,
}

#[derive(Serialize)]
struct User {
    id: u64,
    name: String,
}

// Handler with error handling
async fn create_user(
    State(state): State<AppState>,
    AppJson(payload): AppJson<CreateUser>,
) -> Result<AppJson<User>, AppError> {
    let user = state.db
        .save_user(&payload)
        .await
        .map_err(AppError::DatabaseError)?;
    
    Ok(AppJson(user))
}

let state = AppState {
    db: Arc::new(Database),
};

let app = Router::new()
    .route("/users", post(create_user))
    .with_state(state);

Error logging with middleware

Log errors without exposing details to clients:
use axum::{
    Router,
    routing::get,
    extract::Request,
    middleware::{self, Next},
    response::{IntoResponse, Response},
    http::StatusCode,
};
use std::sync::Arc;

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

impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        // Add error to response extensions for logging
        let mut response = (
            StatusCode::INTERNAL_SERVER_ERROR,
            "Something went wrong",
        ).into_response();
        
        response.extensions_mut().insert(Arc::new(self));
        response
    }
}

// Logging middleware
async fn log_errors(
    request: Request,
    next: Next,
) -> Response {
    let response = next.run(request).await;
    
    // Check if response contains an error
    if let Some(err) = response.extensions().get::<Arc<AppError>>() {
        eprintln!("Error occurred: {:?}", err);
    }
    
    response
}

let app = Router::new()
    .route("/", get(|| async { Result::<_, AppError>::Ok("OK") }))
    .layer(middleware::from_fn(log_errors));

HandleErrorLayer for middleware

Handle errors from Tower middleware:
use axum::{
    Router,
    routing::get,
    error_handling::HandleErrorLayer,
    http::StatusCode,
    response::IntoResponse,
};
use tower::{ServiceBuilder, BoxError};
use std::{borrow::Cow, time::Duration};

async fn handle_timeout_error(err: BoxError) -> impl IntoResponse {
    if err.is::<tower::timeout::error::Elapsed>() {
        return (
            StatusCode::REQUEST_TIMEOUT,
            Cow::from("Request timed out"),
        );
    }

    if err.is::<tower::load_shed::error::Overloaded>() {
        return (
            StatusCode::SERVICE_UNAVAILABLE,
            Cow::from("Service is overloaded"),
        );
    }

    (
        StatusCode::INTERNAL_SERVER_ERROR,
        Cow::from(format!("Unhandled error: {}", err)),
    )
}

let app = Router::new()
    .route("/", get(|| async { "Hello" }))
    .layer(
        ServiceBuilder::new()
            .layer(HandleErrorLayer::new(handle_timeout_error))
            .timeout(Duration::from_secs(30))
    );

Extractor rejections

Customize how extractor failures are handled:
use axum::{
    extract::{rejection::JsonRejection, FromRequest, Request},
    response::{IntoResponse, Response},
    http::StatusCode,
};

struct MyJson<T>(T);

#[axum::async_trait]
impl<S, T> FromRequest<S> for MyJson<T>
where
    axum::Json<T>: FromRequest<S, Rejection = JsonRejection>,
    S: Send + Sync,
{
    type Rejection = Response;

    async fn from_request(
        req: Request,
        state: &S
    ) -> Result<Self, Self::Rejection> {
        match axum::Json::<T>::from_request(req, state).await {
            Ok(value) => Ok(Self(value.0)),
            Err(rejection) => Err((
                StatusCode::BAD_REQUEST,
                format!("Invalid JSON: {}", rejection),
            ).into_response()),
        }
    }
}

Result type aliases

Simplify handler signatures with type aliases:
use axum::{
    response::{IntoResponse, Response},
    http::StatusCode,
};

#[derive(Debug)]
struct AppError;

impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        StatusCode::INTERNAL_SERVER_ERROR.into_response()
    }
}

// Create a type alias
type Result<T> = std::result::Result<T, AppError>;

// Use in handlers
async fn handler() -> Result<String> {
    Ok("Success".to_string())
}

async fn another_handler() -> Result<&'static str> {
    Err(AppError)
}

Error best practices

Never leak sensitive information in error messages:
// ❌ Bad - exposes database details
impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        match self {
            AppError::Database(err) => (
                StatusCode::INTERNAL_SERVER_ERROR,
                format!("Database error: {}", err),  // Exposes internal details!
            ).into_response(),
        }
    }
}

// ✅ Good - generic message for client, log details internally
impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        match self {
            AppError::Database(err) => {
                eprintln!("Database error: {}", err);  // Log internally
                (
                    StatusCode::INTERNAL_SERVER_ERROR,
                    "An internal error occurred",  // Generic message
                ).into_response()
            }
        }
    }
}
Return the correct HTTP status code for each error type:
  • 400 Bad Request - Invalid input
  • 401 Unauthorized - Missing or invalid authentication
  • 403 Forbidden - Authenticated but not authorized
  • 404 Not Found - Resource doesn’t exist
  • 409 Conflict - Request conflicts with current state
  • 422 Unprocessable Entity - Valid syntax but semantic errors
  • 500 Internal Server Error - Server errors
  • 503 Service Unavailable - Temporary unavailability

Next steps

Middleware

Use middleware for error logging and handling

Responses

Learn more about response types