Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
170: Error Handling in Axum Handlers
Imagine you're a waiter at a high-end restaurant. The customer orders a specific bottle of vintage wine. You head back to the cellar, but you find out the bottle is corked, or maybe it's completely sold out. Now, you have a choice: you could walk back to the table and shout, "CELLAR ERROR 404: BOTTLE MISSING!" or you could translate that internal failure into something the customer understands, like "I'm so sorry, we've just run out of that vintage; may I suggest a similar Bordeaux?"
In Axum, your handler is that waiter. The "internal failure" is your Rust Result::Err, and the "customer-facing response" is the HTTP response. The problem is that Axum doesn't know how to "translate" a random std::io::Error or a sqlx::Error into an HTTP response automatically. If you try to return a standard Result where the error type doesn't implement IntoResponse, the compiler will yell at you. I've spent more time fighting that specific compiler error than I'd like to admit.
Giving Your Errors a Voice
The most professional way to handle this is to create a custom error enum. This lets you categorize everything that could go wrong in your domain and then decide exactly how each case should look to the outside world. I usually lean on the thiserror crate for this because it saves me from writing boilerplate Display implementations.
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
Json,
};
use serde_json::json;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum AppError {
#[error("User not found")]
NotFound,
#[error("Database connection failed")]
DatabaseError(#[from] sqlx::Error),
#[error("Invalid input: {0}")]
ValidationError(String),
}
Notice the #[from] attribute on the DatabaseError. This is a lifesaver. It allows you to use the ? operator on database calls, and Rust will automatically wrap those low-level errors into your high-level AppError.
Translating Internal Chaos to HTTP
Now we have to do the "waiter" part: implementing IntoResponse. This is where you map your internal enum variants to actual HTTP status codes and JSON payloads. I prefer returning a JSON object even for errors; it makes the frontend developer's life so much easier.
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let (status, error_message) = match self {
AppError::NotFound => (StatusCode::NOT_FOUND, self.to_string()),
AppError::ValidationError(msg) => (StatusCode::BAD_REQUEST, msg),
AppError::DatabaseError(_) => {
// We don't want to leak raw DB errors to the client for security reasons
(StatusCode::INTERNAL_SERVER_ERROR, "Internal server error".to_string())
}
};
let body = Json(json!({
"error": error_message,
}));
(status, body).into_response()
}
}
See what happened with DatabaseError? I logged the internal error (implicitly via the enum) but sent a generic message to the user. Never leak your database schema or connection strings in an HTTP response. That's an open invitation for hackers.
Cleaning Up the Handlers
Once this infrastructure is in place, your handlers become incredibly clean. You stop worrying about match blocks inside your business logic and just let the ? operator do the heavy lifting. Your handler's return type just needs to be Result<T, AppError>.
async fn get_user_handler(Path(user_id): Path<u64>) -> Result<Json<User>, AppError> {
let user = db::find_user(user_id)
.await
.map_err(|e| match e {
sqlx::Error::RowNotFound => AppError::NotFound,
_ => AppError::DatabaseError(e),
})?;
Ok(Json(user))
}
I'll be honest: sometimes you'll be tempted to just return Result<T, (StatusCode, String)>. It works for tiny projects, but the moment your app grows, you'll find yourself duplicating error strings everywhere. Stick to the enum pattern. It's a bit more setup upfront, but it pays dividends the moment you need to change a status code across ten different endpoints.
📋 Practical Task
Implementing a Validated Product Inventory Error Pipeline
You are building a product management API. You need to create a robust error handling system that distinguishes between different types of failures when updating product stock.
Requirements:
- Create an enum called
InventoryError. - Include three variants:
InsufficientStock(should return400 Bad Request)ProductNotFound(should return404 Not Found)InternalFailure(should return500 Internal Server Error)
- Implement
IntoResponseforInventoryErrorso that it returns a JSON response in the format{"code": "ERROR_CODE", "message": "Human readable message"}. - Write a mock handler
update_stock_handlerthat returnsResult<StatusCode, InventoryError>. - In the handler, simulate a failure by returning
Err(InventoryError::InsufficientStock)using the?operator or a direct return.
There are no comments for now.