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
ResponseErroron your error enum — not on individual structs - Use
thiserrorto deriveDisplayandFromconversions automatically - Match exhaustively in
status_code()so adding new variants forces a status code decision - The
?operator propagates anyResponseErrortype cleanly from async handlers - Never expose internal error messages (sqlx errors, panics) in
error_response()for production