Framework Cookbooks

Rust Actix-web Error Handling: ResponseError Trait and Custom Errors

How to handle HTTP errors in Rust's Actix-web framework — the ResponseError trait, custom error types, error middleware, and type-safe status codes.

The ResponseError Trait

Actix-web's error handling is built around the ResponseError trait. Any type that implements ResponseError can be returned from a handler as an Err(...) variant, and Actix will automatically convert it into an HTTP response.

use actix_web::{
    HttpResponse, ResponseError,
    http::StatusCode,
};
use std::fmt;

// A minimal ResponseError implementation
#[derive(Debug)]
struct MyError {
    message: String,
}

impl fmt::Display for MyError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.message)
    }
}

impl ResponseError for MyError {
    fn status_code(&self) -> StatusCode {
        StatusCode::INTERNAL_SERVER_ERROR
    }

    fn error_response(&self) -> HttpResponse {
        HttpResponse::build(self.status_code())
            .json(serde_json::json!({
                "error": self.message,
            }))
    }
}

The trait requires two methods: status_code() returns the HTTP status code, and error_response() builds the full HTTP response. The default implementation of error_response() uses status_code() and formats the Display string as the body.

Custom Error Types with thiserror

For real applications, use an enum-based error type with the thiserror crate to derive Display automatically:

use actix_web::{
    HttpResponse, ResponseError,
    http::StatusCode,
};
use thiserror::Error;
use serde_json::json;

#[derive(Debug, Error)]
pub enum ApiError {
    #[error("Resource not found: {id}")]
    NotFound { id: i64 },

    #[error("Authentication required")]
    Unauthorized,

    #[error("Permission denied")]
    Forbidden,

    #[error("Validation failed: {message}")]
    ValidationError { message: String },

    #[error("Database error")]
    DatabaseError(#[from] sqlx::Error),
}

impl ResponseError for ApiError {
    fn status_code(&self) -> StatusCode {
        match self {
            ApiError::NotFound { .. }   => StatusCode::NOT_FOUND,
            ApiError::Unauthorized      => StatusCode::UNAUTHORIZED,
            ApiError::Forbidden         => StatusCode::FORBIDDEN,
            ApiError::ValidationError { .. } => StatusCode::UNPROCESSABLE_ENTITY,
            ApiError::DatabaseError(_)  => StatusCode::INTERNAL_SERVER_ERROR,
        }
    }

    fn error_response(&self) -> HttpResponse {
        let body = match self {
            ApiError::ValidationError { message } => json!({
                "error": "Validation failed",
                "detail": message,
            }),
            ApiError::DatabaseError(_) => json!({ "error": "Internal server error" }),
            _ => json!({ "error": self.to_string() }),
        };
        HttpResponse::build(self.status_code()).json(body)
    }
}

The #[from] sqlx::Error annotation auto-generates a From<sqlx::Error> impl, so database errors propagate with ? without explicit conversion.

Error Middleware

Actix-web's ErrorHandlers middleware lets you intercept responses at the middleware layer and transform them:

use actix_web::{
    App, HttpServer, web,
    middleware::ErrorHandlers,
    dev::ServiceResponse,
    http::StatusCode,
};

fn add_error_header<B>(mut res: ServiceResponse<B>) -> actix_web::Result<
    actix_web::middleware::ErrorHandlerResponse<B>
> {
    res.response_mut().headers_mut().insert(
        actix_web::http::header::CONTENT_TYPE,
        actix_web::http::header::HeaderValue::from_static("application/json"),
    );
    Ok(actix_web::middleware::ErrorHandlerResponse::Response(
        res.map_into_left_body(),
    ))
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .wrap(
                ErrorHandlers::new()
                    .handler(StatusCode::NOT_FOUND, add_error_header)
                    .handler(StatusCode::INTERNAL_SERVER_ERROR, add_error_header),
            )
            .service(web::resource("/users/{id}").get(get_user))
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

The ? Operator and Error Propagation

The ? operator works in Actix handlers because handlers return actix_web::Result<T>, which is Result<T, actix_web::Error>. Use From trait implementations to convert domain errors to actix_web::Error:

use actix_web::web;

async fn get_user(
    path: web::Path<i64>,
    db: web::Data<DbPool>,
) -> actix_web::Result<web::Json<User>> {
    let id = path.into_inner();

    // ? converts ApiError (which impls ResponseError) into actix_web::Error
    let user = db.find_user(id).await
        .map_err(|_| ApiError::DatabaseError(sqlx::Error::RowNotFound))?
        .ok_or(ApiError::NotFound { id })?;

    Ok(web::Json(user))
}

Any type that implements ResponseError can be converted to actix_web::Error automatically via Into<actix_web::Error>. This keeps handler code clean — no match blocks, no unwrap(), just ?.

Type-Safe Status Codes

Actix-web's StatusCode type from actix_web::http::StatusCode (re-exported from the http crate) provides named constants for all standard codes:

use actix_web::http::StatusCode;

// All standard codes are named constants
StatusCode::OK                    // 200
StatusCode::CREATED               // 201
StatusCode::NO_CONTENT            // 204
StatusCode::BAD_REQUEST           // 400
StatusCode::UNAUTHORIZED          // 401
StatusCode::FORBIDDEN             // 403
StatusCode::NOT_FOUND             // 404
StatusCode::UNPROCESSABLE_ENTITY  // 422
StatusCode::TOO_MANY_REQUESTS     // 429
StatusCode::INTERNAL_SERVER_ERROR // 500

// Custom codes (rarely needed)
let code = StatusCode::from_u16(418).unwrap();
assert_eq!(code.as_u16(), 418);

The StatusCode::from_u16() method returns Err for invalid codes (outside 100-999), preventing invalid status codes at runtime. Using named constants in match arms also gives you exhaustiveness checking — if Actix adds a new commonly-used code and you have a wildcard arm, the compiler does not warn, but explicit arms make code self-documenting.

Key Takeaways

  • Implement ResponseError on your error enum — not on individual structs
  • Use thiserror to derive Display and From conversions automatically
  • Match exhaustively in status_code() so adding new variants forces a status code decision
  • The ? operator propagates any ResponseError type cleanly from async handlers
  • Never expose internal error messages (sqlx errors, panics) in error_response() for production

Related Protocols

Related Glossary Terms

More in Framework Cookbooks