Basic error handling
The simplest way to handle errors is usingResult 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));
Ok and Err variants must implement IntoResponse.
Custom error types
Create application-specific error types that implementIntoResponse:
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:- Custom rejection
- Using derive macro
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()),
}
}
}
use axum::{
extract::{rejection::JsonRejection, FromRequest},
response::{IntoResponse, Response},
http::StatusCode,
};
#[derive(FromRequest)]
#[from_request(via(axum::Json), rejection(ApiError))]
struct ApiJson<T>(T);
struct ApiError(JsonRejection);
impl From<JsonRejection> for ApiError {
fn from(rejection: JsonRejection) -> Self {
Self(rejection)
}
}
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
(
StatusCode::BAD_REQUEST,
format!("JSON error: {}", self.0),
).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
Don't expose internal errors
Don't expose internal errors
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()
}
}
}
}
Use appropriate status codes
Use appropriate status codes
Return the correct HTTP status code for each error type:
400 Bad Request- Invalid input401 Unauthorized- Missing or invalid authentication403 Forbidden- Authenticated but not authorized404 Not Found- Resource doesn’t exist409 Conflict- Request conflicts with current state422 Unprocessable Entity- Valid syntax but semantic errors500 Internal Server Error- Server errors503 Service Unavailable- Temporary unavailability
Next steps
Middleware
Use middleware for error logging and handling
Responses
Learn more about response types