salvo-error-handling
Handle errors gracefully with custom error types, status codes, and error pages. Use for building robust APIs with proper error responses.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Handle errors gracefully with custom error types, status codes, and error pages. Use for building robust APIs with proper error responses.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
Guide for using RBatis v4 Rust ORM framework. Use when implementing database CRUD operations, writing dynamic SQL with py_sql or html_sql macros, configuring database connections, managing transactions, implementing interceptors, syncing table structures, or integrating RBatis into Genies microservices. Also use when the user asks about RBatis usage patterns, query building, or database operations in Rust.
Genies 前后端接口规范。Use when designing API contracts, defining field naming conventions, date/time formats, response models, pagination, error handling, or ID strategies between Genies backend and frontend (Web/Android/OHOS).
Guide for using flyway-rs database migration framework with RBatis. Use when implementing database schema migrations, managing SQL changelog files, configuring migration runners, handling multi-database migrations, or integrating database versioning into Genies microservices.
Guide for developing Rust microservices using Genies framework following Java DDD layering principles. Use when creating new microservices, designing aggregate roots, implementing domain events, organizing service layers, setting up Flyway migrations, or structuring a DDD-based Genies project.
Genies framework unified skill hub. Use when you need to find the right skill for any Genies framework task, including authentication, authorization, caching, configuration, database, DDD microservices, Dapr messaging, macros, K8s deployment, testing, API conventions, gateway proxy, or Salvo web framework features. Also use when the user asks about Genies framework capabilities, asks which skill to use, or wants a quick overview of available Genies skills.
Guide for using the Genies Rust microservice framework with DDD and Dapr. Use when developing with Genies, creating aggregates, domain events, Dapr subscriptions, Casbin field-level permissions, configuration management, or when the user asks about Genies framework usage patterns.
| name | salvo-error-handling |
| description | Handle errors gracefully with custom error types, status codes, and error pages. Use for building robust APIs with proper error responses. |
| version | 0.89.3 |
| tags | ["core","error-handling","status-code"] |
This skill helps implement proper error handling in Salvo applications.
In Salvo, error handling covers three categories:
The simplest way to return errors:
use salvo::prelude::*;
#[handler]
async fn get_user(req: &mut Request) -> Result<Json<User>, StatusError> {
let id = req.param::<i64>("id")
.ok_or_else(|| StatusError::bad_request().brief("Missing user ID"))?;
let user = find_user(id).await
.ok_or_else(|| StatusError::not_found().brief("User not found"))?;
Ok(Json(user))
}
// Client errors (4xx)
StatusError::bad_request() // 400
StatusError::unauthorized() // 401
StatusError::forbidden() // 403
StatusError::not_found() // 404
StatusError::method_not_allowed() // 405
StatusError::conflict() // 409
StatusError::unprocessable_entity() // 422
// Server errors (5xx)
StatusError::internal_server_error() // 500
StatusError::not_implemented() // 501
StatusError::bad_gateway() // 502
StatusError::service_unavailable() // 503
// Add details
StatusError::bad_request()
.brief("Invalid input")
.cause("Field 'email' is required")
Enable features for popular error handling crates:
[dependencies]
salvo = { version = "0.89.3", features = ["anyhow", "eyre"] }
anyhow = "1"
eyre = "0.6"
use salvo::prelude::*;
use anyhow::Context;
#[handler]
async fn process_data() -> Result<String, anyhow::Error> {
let data = fetch_data().await
.context("Failed to fetch data")?;
let result = process(data)
.context("Failed to process data")?;
Ok(result)
}
use salvo::prelude::*;
use eyre::WrapErr;
#[handler]
async fn process_data() -> eyre::Result<String> {
let data = fetch_data().await
.wrap_err("Failed to fetch data")?;
Ok(data)
}
Define custom errors that implement Writer for full control:
use salvo::prelude::*;
use serde::Serialize;
#[derive(Debug)]
enum AppError {
NotFound(String),
ValidationError(String),
DatabaseError(String),
Unauthorized,
}
#[async_trait]
impl Writer for AppError {
async fn write(self, _req: &mut Request, _depot: &mut Depot, res: &mut Response) {
let (status, message) = match &self {
AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg.clone()),
AppError::ValidationError(msg) => (StatusCode::BAD_REQUEST, msg.clone()),
AppError::DatabaseError(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg.clone()),
AppError::Unauthorized => (StatusCode::UNAUTHORIZED, "Unauthorized".to_string()),
};
res.status_code(status);
res.render(Json(serde_json::json!({
"error": message,
"code": status.as_u16()
})));
}
}
#[handler]
async fn get_user(req: &mut Request) -> Result<Json<User>, AppError> {
let id = req.param::<i64>("id")
.ok_or_else(|| AppError::ValidationError("Missing user ID".to_string()))?;
let user = find_user(id).await
.map_err(|e| AppError::DatabaseError(e.to_string()))?
.ok_or_else(|| AppError::NotFound(format!("User {} not found", id)))?;
Ok(Json(user))
}
Use thiserror for ergonomic error definitions:
use salvo::prelude::*;
use thiserror::Error;
#[derive(Error, Debug)]
enum ApiError {
#[error("Resource not found: {0}")]
NotFound(String),
#[error("Validation error: {0}")]
Validation(String),
#[error("Database error: {0}")]
Database(#[from] sqlx::Error),
#[error("Unauthorized")]
Unauthorized,
}
#[async_trait]
impl Writer for ApiError {
async fn write(self, _req: &mut Request, _depot: &mut Depot, res: &mut Response) {
let status = match &self {
ApiError::NotFound(_) => StatusCode::NOT_FOUND,
ApiError::Validation(_) => StatusCode::BAD_REQUEST,
ApiError::Database(_) => StatusCode::INTERNAL_SERVER_ERROR,
ApiError::Unauthorized => StatusCode::UNAUTHORIZED,
};
res.status_code(status);
res.render(Json(serde_json::json!({
"error": self.to_string()
})));
}
}
Use CatchPanic middleware to handle panics gracefully:
use salvo::prelude::*;
use salvo::catcher::CatchPanic;
#[handler]
async fn may_panic() -> &'static str {
panic!("Something went wrong!");
}
#[tokio::main]
async fn main() {
let router = Router::new()
.hoop(CatchPanic::new()) // Catch panics globally
.get(may_panic);
let acceptor = TcpListener::new("0.0.0.0:8080").bind().await;
Server::new(acceptor).serve(router).await;
}
Create custom error pages for specific status codes:
use salvo::prelude::*;
use salvo::catcher::Catcher;
#[handler]
async fn handle_404(res: &mut Response, ctrl: &mut FlowCtrl) {
if res.status_code() == Some(StatusCode::NOT_FOUND) {
res.render("Custom 404 - Page Not Found");
ctrl.skip_rest();
}
}
#[handler]
async fn handle_500(res: &mut Response, ctrl: &mut FlowCtrl) {
if res.status_code().map_or(false, |c| c.is_server_error()) {
res.render("Custom 500 - Internal Server Error");
ctrl.skip_rest();
}
}
fn create_service(router: Router) -> Service {
Service::new(router).catcher(
Catcher::default()
.hoop(handle_404)
.hoop(handle_500)
)
}
#[tokio::main]
async fn main() {
let router = Router::new().get(hello);
let service = create_service(router);
let acceptor = TcpListener::new("0.0.0.0:8080").bind().await;
Server::new(acceptor).serve(service).await;
}
use salvo::prelude::*;
use serde::Serialize;
#[derive(Serialize)]
struct ErrorResponse {
code: u16,
error: String,
message: String,
#[serde(skip_serializing_if = "Option::is_none")]
details: Option<Vec<String>>,
}
impl ErrorResponse {
fn new(status: StatusCode, error: &str, message: &str) -> Self {
Self {
code: status.as_u16(),
error: error.to_string(),
message: message.to_string(),
details: None,
}
}
fn with_details(mut self, details: Vec<String>) -> Self {
self.details = Some(details);
self
}
}
#[handler]
async fn api_handler() -> Result<Json<Data>, (StatusCode, Json<ErrorResponse>)> {
let data = fetch_data().await.map_err(|e| {
let error = ErrorResponse::new(
StatusCode::INTERNAL_SERVER_ERROR,
"DATABASE_ERROR",
&e.to_string(),
);
(StatusCode::INTERNAL_SERVER_ERROR, Json(error))
})?;
Ok(Json(data))
}
Log errors with context for debugging:
use salvo::prelude::*;
use tracing::{error, warn};
#[handler]
async fn handler(req: &mut Request) -> Result<String, StatusError> {
let result = process_request(req).await;
match result {
Ok(data) => Ok(data),
Err(e) => {
// Log the error with context
error!(
error = %e,
path = %req.uri().path(),
method = %req.method(),
"Request processing failed"
);
Err(StatusError::internal_server_error()
.brief("An error occurred processing your request"))
}
}
}
CatchPanic as safety net, not normal flow