How does axum::extract::rejection::Rejection differ from anyhow::Error for handler error handling?
Rejection is Axum's type-system mechanism for signaling that an extractor failed to extract data from a request, while anyhow::Error is a general-purpose application error type for propagating failures throughout your codebaseāthey serve fundamentally different purposes in the request handling pipeline. A Rejection tells Axum's routing system "this handler isn't appropriate for this request," enabling fallback to other handlers or custom error responses, whereas anyhow::Error represents actual application errors that should be converted into HTTP responses. Use Rejection only for extractor failures that should trigger routing fallbacks; use anyhow::Error (or custom error types) for application logic errors that need proper error handling and response generation.
What Rejection Represents
use axum::{
extract::{Path, Query, rejection::PathRejection},
http::StatusCode,
response::{IntoResponse, Response},
};
async fn get_user(Path(id): Path<u64>) -> Result<String, Rejection> {
// Path extractor succeeded, return the user
Ok(format!("User {}", id))
}
// Rejection is returned when Path<u64> fails:
// - Path segment is missing
// - Path segment isn't a valid u64
// - Path doesn't match the route pattern
// Rejections are created by extractors when they can't extract
// You rarely create them manually
use axum::extract::rejection::Rejection;
async fn manual_rejection() -> Result<String, Rejection> {
// You can create a rejection, but it's unusual
Err(Rejection::from(PathRejection::MissingPathParams))
}Rejection is an extractor failure signal, not a general error type.
Rejection vs Application Errors
use axum::{
extract::Path,
http::StatusCode,
response::{IntoResponse, Response},
};
use anyhow::Error;
// WRONG: Using Rejection for application errors
async fn bad_example(Path(id): Path<u64>) -> Result<String, Rejection> {
// This is misuse of Rejection
let user = find_user(id).map_err(|e| {
// Don't convert application errors to Rejection!
// Rejection is for extractor failures, not application errors
Rejection::from(/* ... */) // This loses error context
})?;
Ok(user.name)
}
// CORRECT: Using anyhow::Error for application errors
async fn good_example(Path(id): Path<u64>) -> Result<String, Error> {
let user = find_user(id)?; // Application error propagates naturally
Ok(user.name)
}
struct User { name: String }
fn find_user(_id: u64) -> Result<User, Error> {
Err(Error::msg("user not found"))
}Rejection for extractor failures; anyhow::Error for application logic errors.
The Rejection Type
use axum::extract::rejection::Rejection;
// Rejection wraps concrete rejection types
// It's actually a type-erased wrapper around specific rejections:
// Common rejection types:
// - PathRejection: Path extractor failed
// - QueryRejection: Query extractor failed
// - JsonRejection: Json body parsing failed
// - ExtensionRejection: Extension not found
// Rejection is created by extractors:
use axum::extract::{Path, FromRequest, Request};
use axum::extract::rejection::PathRejection;
async fn path_example(
// Path extractor can fail with PathRejection
Path(id): Path<u64>,
) -> String {
format!("User {}", id)
}
// When Path<u64> fails (e.g., non-numeric path param),
// it internally creates a RejectionRejection is a type-erased container for specific rejection types.
How Rejection Enables Fallback Routing
use axum::{
extract::Path,
routing::get,
Router,
http::StatusCode,
response::IntoResponse,
};
// Rejection allows Axum to try other handlers
async fn specific_handler(Path(id): Path<u64>) -> Result<String, Rejection> {
// If Path<u64> fails, Axum can try another handler
Ok(format!("Specific user {}", id))
}
async fn fallback_handler() -> &'static str {
"Fallback for any path"
}
let app = Router::new()
.route("/users/:id", get(specific_handler))
.fallback(fallback_handler);
// If Path<u64> extraction fails, Axum calls fallback_handler
// This is the key feature of Rejection - it enables routing decisionsRejections integrate with Axum's routing system for fallback behavior.
anyhow::Error for Application Logic
use axum::{
extract::Path,
http::StatusCode,
response::{IntoResponse, Response},
};
use anyhow::Error;
struct User {
id: u64,
name: String,
}
fn find_user(id: u64) -> Result<User, Error> {
// Application logic errors use anyhow
if id == 0 {
return Err(Error::msg("invalid user id"));
}
Ok(User { id, name: "Alice".to_string() })
}
async fn get_user(Path(id): Path<u64>) -> Result<String, Error> {
// Extractor succeeded, now application logic
let user = find_user(id)?; // anyhow::Error propagates
Ok(user.name)
}anyhow::Error carries application error context and backtraces.
Converting Errors to Responses
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
};
use anyhow::Error;
use std::fmt;
// Convert anyhow::Error to HTTP response
impl IntoResponse for Error {
fn into_response(self) -> Response {
// Application error becomes 500 Internal Server Error
let body = format!("Internal error: {}", self);
(StatusCode::INTERNAL_SERVER_ERROR, body).into_response()
}
}
// Rejection has default IntoResponse (usually 404 or 400)
// You can customize rejection responses:
use axum::extract::rejection::Rejection;
impl IntoResponse for Rejection {
fn into_response(self) -> Response {
// Default behavior: convert to appropriate status code
// 400 for JsonRejection, 404 for missing path, etc.
StatusCode::BAD_REQUEST.into_response()
}
}
async fn handler() -> Result<String, Error> {
Err(Error::msg("something went wrong"))
}Both implement IntoResponse, but with different default behaviors.
Custom Error Types
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
};
use std::fmt;
// Custom application error type
enum AppError {
NotFound(String),
Unauthorized,
Internal(Error),
}
impl IntoResponse for AppError {
fn into_response(self) -> Response {
match self {
AppError::NotFound(msg) => {
(StatusCode::NOT_FOUND, msg).into_response()
}
AppError::Unauthorized => {
(StatusCode::UNAUTHORIZED, "Please log in").into_response()
}
AppError::Internal(err) => {
(StatusCode::INTERNAL_SERVER_ERROR, format!("Error: {}", err)).into_response()
}
}
}
}
// Application handler with custom error
async fn get_user(Path(id): Path<u64>) -> Result<String, AppError> {
let user = find_user(id).map_err(AppError::Internal)?;
Ok(user.name)
}
fn find_user(id: u64) -> Result<User, Error> {
Err(Error::msg("database error"))
}
use axum::extract::Path;
struct User { name: String }Custom error types provide better control than raw anyhow::Error.
Rejection Customization with FromRequest
use axum::{
extract::{FromRequest, Path, Request},
http::StatusCode,
response::{IntoResponse, Response},
};
use axum::extract::rejection::PathRejection;
// Custom extractor with better rejection handling
struct ValidatedPath<T>(T);
impl<S, T> FromRequest<S> for ValidatedPath<T>
where
S: Send + Sync + 'static,
Path<T>: FromRequest<S, Rejection = PathRejection>,
{
type Rejection = (StatusCode, String); // Custom rejection type!
async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
let path = Path::<T>::from_request(req, state)
.await
.map_err(|rejection| {
// Convert rejection to custom error response
(StatusCode::BAD_REQUEST, format!("Invalid path: {}", rejection))
})?;
Ok(ValidatedPath(path.0))
}
}
async fn user_handler(ValidatedPath(id): ValidatedPath<u64>) -> String {
format!("User {}", id)
}Custom extractors can transform rejections into application errors.
Layered Error Handling
use axum::{
extract::Path,
http::StatusCode,
response::{IntoResponse, Response},
};
use anyhow::Error;
use tower::ServiceBuilder;
use tower_http::trace::TraceLayer;
// Layer 1: Extractor failures -> Rejection
// Layer 2: Application errors -> anyhow::Error (or custom)
// Layer 3: IntoResponse conversion
async fn get_user(Path(id): Path<u64>) -> Result<String, Error> {
// If Path fails, Rejection is returned (layer 1)
// If find_user fails, Error is returned (layer 2)
let user = find_user(id)?;
Ok(user.name)
}
// Error handling flow:
// 1. Request arrives
// 2. Path extractor attempts to parse :id as u64
// 3a. If parsing fails: Rejection returned, Axum can try fallback
// 3b. If parsing succeeds: Handler body executes
// 4. If find_user fails: Error returned, converted via IntoResponse
fn find_user(_id: u64) -> Result<User, Error> {
Err(Error::msg("not found"))
}
struct User { name: String }Rejection and application errors operate at different layers.
When Rejection Flows Up
use axum::{
extract::{Path, FromRequest, Request},
response::IntoResponse,
};
// Rejection typically comes from extractors
async fn handler(
Path(id): Path<u64>, // Could reject
) -> Result<String, Rejection> {
// If Path extractor succeeded, handler runs
Ok(format!("User {}", id))
}
// You can also manually return Rejection
use axum::extract::rejection::JsonRejection;
use axum::Json;
async fn manual_rejection(
Json(data): Json<serde_json::Value>,
) -> Result<String, Rejection> {
// JSON parsing happens automatically
// If it fails, JsonRejection is returned
// You could manually create a rejection, but it's rare
// Manual rejections are usually for custom extractors
Ok("success".to_string())
}Rejection usually flows from extractors, not manual creation.
Mixing Rejection and anyhow
use axum::{
extract::Path,
http::StatusCode,
response::{IntoResponse, Response},
};
use anyhow::Error;
// Common pattern: Extractor errors as Rejection, app errors as anyhow
async fn get_user(
Path(id): Path<u64>, // Could fail with Rejection
) -> Result<Json<User>, AppError> {
// Application error uses custom type, not Rejection
let user = find_user(id)?;
Ok(Json(user))
}
// Or combine with proper error handling:
enum AppError {
Rejection(Rejection),
Internal(Error),
}
use axum::extract::rejection::Rejection;
use axum::Json;
impl IntoResponse for AppError {
fn into_response(self) -> Response {
match self {
AppError::Rejection(r) => r.into_response(),
AppError::Internal(e) => {
(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response()
}
}
}
}
struct User;
fn find_user(_id: u64) -> Result<User, Error> {
Err(Error::msg("not found"))
}Keep Rejection for extractor failures, application errors separate.
Rejection Types Overview
use axum::extract::rejection::*;
// Specific rejection types from various extractors:
//
// PathRejection - Path extractor failed
// QueryRejection - Query extractor failed
// JsonRejection - Json body parsing failed
// FormRejection - Form parsing failed
// ExtensionRejection - Extension missing
// BodyRejection - Body extraction failed
// HeadersRejection - Headers extraction failed
// Each has specific variants:
//
// JsonRejection::JsonDataError - Invalid JSON syntax
// JsonRejection::MissingJsonContentType - Wrong content-type header
// JsonRejection::BodyAlreadyExtracted - Body consumed elsewhere
// JsonRejection::LengthLimitExceeded - Request body too large
// All rejection types convert to the erased Rejection typeSpecific rejection types provide detailed failure information.
Practical Pattern: Typed Errors
use axum::{
extract::Path,
http::StatusCode,
response::{IntoResponse, Response},
};
use anyhow::Error;
// Best practice: Use typed errors for clarity
enum ApiError {
// Application errors
NotFound(String),
BadRequest(String),
Internal(Error),
// Wrapper for rejections when needed
Rejection(Rejection),
}
use axum::extract::rejection::Rejection;
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
match self {
ApiError::NotFound(msg) => {
(StatusCode::NOT_FOUND, msg).into_response()
}
ApiError::BadRequest(msg) => {
(StatusCode::BAD_REQUEST, msg).into_response()
}
ApiError::Internal(err) => {
(StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response()
}
ApiError::Rejection(rejection) => {
// Let Axum handle rejection
rejection.into_response()
}
}
}
}
// Handler is clear about error types
async fn get_user(Path(id): Path<u64>) -> Result<String, ApiError> {
let user = find_user(id)
.map_err(|e| ApiError::NotFound(e.to_string()))?;
Ok(user.name)
}
fn find_user(_id: u64) -> Result<User, Error> {
Err(Error::msg("not found"))
}
struct User { name: String }Typed errors provide better documentation and handling.
Synthesis
Quick reference:
use axum::extract::rejection::Rejection;
use axum::extract::Path;
use anyhow::Error;
// Rejection: Extractor failure signal
// - Created by extractors (Path, Query, Json, etc.)
// - Enables routing fallbacks
// - Converted to HTTP responses (400, 404, etc.)
// - Used in extractor error handling
// - NOT for application logic errors
async fn rejection_example(Path(id): Path<u64>) -> Result<String, Rejection> {
// If Path<u64> fails, Rejection is returned
// Axum can try other handlers or fallbacks
Ok(format!("User {}", id))
}
// anyhow::Error: Application error type
// - Created by application code
// - Carries context and backtraces
// - Converted to HTTP responses via IntoResponse
// - Used for business logic failures
// - NOT for extractor failures
async fn anyhow_example(Path(id): Path<u64>) -> Result<String, Error> {
// Application logic errors use anyhow
let user = database::find_user(id)?;
Ok(user.name)
}
// Combined pattern:
enum AppError {
Application(Error),
Extraction(Rejection),
}
// Key distinction:
// Rejection -> "This handler isn't right for this request"
// anyhow::Error -> "Something went wrong in application logic"Key insight: Rejection and anyhow::Error serve distinct roles in Axum applications. Rejection is the type system's way of expressing "this extractor couldn't extract from this request"āit's a signal to the routing layer that enables fallback to other handlers or custom error responses. anyhow::Error (or custom error types) represents actual application failures: database errors, validation errors, business rule violations. The mistake to avoid is using Rejection for application errorsāthat conflates two different concerns and loses the ability to properly handle errors. Instead, let extractors return Rejection for extraction failures, return application errors from your handlers using anyhow::Error or custom types, and implement IntoResponse to convert those errors to appropriate HTTP responses.
