错误处理是 Rust 最独特的设计之一:没有异常,没有 null,用类型系统强制你面对失败。这不是负担——这是编译器在替你排雷。从 panic! 到 anyhow,这篇文章覆盖你需要的全部。
Rust 将错误分为两大类,这是它与其他语言最根本的区别:
panic!Result<T,E>// 错误是类型的一部分
fn read_file(path: &str)
-> Result<String, io::Error>
{
let content = fs::read_to_string(path)?;
Ok(content)
}
// 调用者必须处理错误
match read_file("config.toml") {
Ok(s) => println!("{s}"),
Err(e) => eprintln!("Error: {e}"),
}
# 错误是隐式的,调用者可能忽略
def read_file(path):
with open(path) as f:
return f.read()
# 没有强制处理,可能突然崩溃
content = read_file("config.toml")
# 如果文件不存在 → RuntimeError
# 但函数签名完全看不出来
错误是函数签名的一部分。Result 告诉你"这里可能失败",你无法假装没看见。编译器是你的盟友。
panic vs Result 不是随意选择——它是设计决策。如果你选 Result,调用者必须处理;如果你选 panic,说明这种情况"不该发生"。
Result 是普通枚举,? 运算符编译为普通的分支跳转。没有异常表、没有栈展开(正常路径),性能完全可预测。
Rust 的洞察:大多数语言的错误处理问题不是"该用什么机制",而是"调用者不知道需要处理错误"。Rust 把这个信息编码进类型系统,让不可能被忽略。
panic! 表示程序遇到了不可能恢复的状态——这通常意味着代码有 bug,继续运行只会产生更严重的问题。
| 场景 | 示例 | 原因 |
|---|---|---|
| 数组越界 | v[100] 在长度为 5 的 Vec 上 | 逻辑 bug,无法继续 |
| 除以零 | 1 / 0 | 数学上不可能 |
| 不变量被破坏 | 解析后仍 None | 逻辑假设不成立 |
| 初始化失败 | 数据库连接池创建失败 | 程序无法启动 |
| 示例/原型代码 | unwrap() 在 demo 中 | 简化代码,非生产 |
// Cargo.toml
[profile.dev]
panic = "unwind" # 默认
// 行为:
// 1. 打印 panic 信息
// 2. 栈展开(调用 Drop)
// 3. 释放资源
// 4. 退出线程
// 优点:资源可清理、可 catch_unwind
// 缺点:二进制更大、稍慢
// Cargo.toml
[profile.release]
panic = "abort"
// 行为:
// 1. 打印 panic 信息
// 2. 直接终止进程
// 3. 不调用 Drop
// 优点:更小二进制、更快
// 缺点:无清理、无法 catch
⚠️ 嵌入式 / 高性能场景:生产发布时 panic = "abort" 可以显著减小二进制体积(去掉 unwind 表),但代价是 panic 时不会调用析构函数。如果你依赖 RAII 清理,需谨慎。
use std::panic;
let result = panic::catch_unwind(|| {
println!("before panic");
panic!("oh no!");
println!("never reached");
});
match result {
Ok(_) => println!("正常完成"),
Err(_) => println!("捕获到 panic!"),
}
// 输出:
// before panic
// 捕获到 panic!
🚨 catch_unwind 不是 try-catch! 它的设计用途仅限于:
不要用它做常规错误控制流——那是 Result 的工作。
Result 是 Rust 错误处理的基石——一个简单的枚举,却承载了丰富的组合子语义。
| 组合子 | 签名 | 用途 |
|---|---|---|
map | Result<T,E> → (T→U) → Result<U,E> | 转换成功值 |
map_err | Result<T,E> → (E→F) → Result<T,F> | 转换错误 |
and_then | Result<T,E> → (T→Result<U,E>) → Result<U,E> | 链式操作(flatMap) |
or_else | Result<T,E> → (E→Result<T,F>) → Result<T,F> | 错误恢复 |
unwrap | Result<T,E> → T | 断言成功,否则 panic |
expect | Result<T,E> → &str → T | 同上,带自定义消息 |
unwrap_or | Result<T,E> → T → T | 失败时提供默认值 |
unwrap_or_else | Result<T,E> → (E→T) → T | 失败时计算默认值 |
unwrap_or_default | Result<T,E> → T | 失败时用 Default |
is_ok / is_err | Result<T,E> → bool | 判断状态 |
ok / err | Result<T,E> → Option<T> / Option<E> | 转为 Option |
use std::fs;
// ❌ 嵌套 match —— 金字塔厄运
fn parse_config_v1(path: &str) -> Result<Config, AppError> {
match fs::read_to_string(path) {
Ok(content) => match toml::from_str(&content) {
Ok(config) => Ok(config),
Err(e) => Err(AppError::Parse(e)),
},
Err(e) => Err(AppError::Io(e)),
}
}
// ✅ 组合子风格 —— 线性、可读
fn parse_config_v2(path: &str) -> Result<Config, AppError> {
fs::read_to_string(path)
.map_err(AppError::Io)
.and_then(|content| {
toml::from_str(&content)
.map_err(AppError::Parse)
})
}
⚠️ 生产代码原则:
unwrap() —— 仅在"不可能失败"时使用(如 Ok(42))expect("reason") —— 优于 unwrap,因为消息解释了"为什么不可能失败"unwrap(),没毛病expect() 优于 unwrap(),它提供调试上下文// ✅ 好:解释了为什么这里不可能失败
let port: u16 = env::var("PORT")
.expect("PORT must be set by the deployment script");
// ❌ 差:没有上下文
let port: u16 = env::var("PORT").unwrap();
// ✅ 测试中:随意 unwrap
#[test]
fn test_addition() {
assert_eq!(parse("1+2").unwrap(), 3);
}
? 是 Rust 最优雅的语法糖——它把"检查错误、提前返回"压缩成一个字符。
// 这一行:
let content = fs::read_to_string(path)?;
// 等价于:
let content = match fs::read_to_string(path) {
Ok(v) => v,
Err(e) => return Err(e.into()),
};
注意 .into()——? 会自动做类型转换,前提是错误类型实现了 Into。这让它可以跨错误类型传播。
use std::fs;
use std::io;
// 无 ? 版本
fn read_and_parse(path: &str) -> Result<Config, io::Error> {
let content = match fs::read_to_string(path) {
Ok(c) => c,
Err(e) => return Err(e),
};
let config = match parse(&content) {
Ok(c) => c,
Err(e) => return Err(e),
};
Ok(config)
}
// ? 链版本
fn read_and_parse(path: &str) -> Result<Config, io::Error> {
let content = fs::read_to_string(path)?;
let config = parse(&content)?;
Ok(config)
}
// 甚至一行搞定
fn read_and_parse(path: &str) -> Result<Config, io::Error> {
parse(&fs::read_to_string(path)?)
}
💡 ? 也适用于 Option! 在返回 Option 的函数中,? 会在 None 时提前返回 None。
fn last_char_of_first_line(text: &str) -> Option<char> {
let first_line = text.lines().next()?; // None → return None
first_line.chars().last() // Some(c) or None
}
#![feature(try_blocks)]
fn try_operation() -> Result<i32, io::Error> {
let result: Result<i32, io::Error> = try {
let a = fs::read_to_string("a.txt")?;
let b = fs::read_to_string("b.txt")?;
a.len() + b.len()
};
result
}
// try 块让你在同一个函数中使用 ?
// 而不需要每个子函数都返回 Result
⚠️ try 块目前仍为 nightly 特性(#![feature(try_blocks)])。稳定前,使用闭包作为替代:
// 稳定版替代方案:闭包 + ?
let result = (|| -> Result<_, io::Error> {
let a = fs::read_to_string("a.txt")?;
let b = fs::read_to_string("b.txt")?;
Ok(a.len() + b.len())
})();
Rust 没有 null。Option<T> 就是 Rust 对"值可能不存在"的回答。
| 组合子 | 签名 | 用途 |
|---|---|---|
map | Option<T> → (T→U) → Option<U> | 转换 Some 中的值 |
and_then | Option<T> → (T→Option<U>) → Option<U> | 链式操作 |
or_else | Option<T> → (→Option<T>) → Option<T> | None 时的备选方案 |
unwrap_or | Option<T> → T → T | 提供默认值 |
unwrap_or_else | Option<T> → (→T) → T | 延迟计算默认值 |
unwrap_or_default | Option<T> → T | Default trait 默认值 |
ok_or | Option<T> → E → Result<T,E> | 转 Result:None→Err |
ok_or_else | Option<T> → (→E) → Result<T,E> | 延迟计算错误 |
filter | Option<T> → (&T→bool) → Option<T> | 条件过滤 |
is_some / is_none | Option<T> → bool | 判断 |
// Option → Result
fn find_user(id: u64) -> Option<User> { ... }
// 需要给 None 一个有意义的错误
let user = find_user(42)
.ok_or(AppError::UserNotFound(42))?;
// Result → Option(丢弃错误信息)
let maybe_port: Option<u16> = env::var("PORT")
.ok() // Err → None
.and_then(|s| s.parse().ok()); // ParseError → None
// ⚠️ ok() 丢弃了错误,只在"不关心错误原因"时使用
struct Config {
database_url: Option<String>,
pool_size: Option<usize>,
timeout_secs: Option<u64>,
}
// 环境变量覆盖配置
fn resolve_pool_size(config: &Config) -> usize {
env::var("DB_POOL_SIZE")
.ok()
.and_then(|s| s.parse().ok())
.or(config.pool_size)
.unwrap_or(10) // 最终默认值
}
// filter:条件过滤
fn find_admin(users: &[User]) -> Option<&User> {
users.iter()
.find(|u| u.is_admin)
.filter(|u| u.is_active) // 找到 admin 且必须是 active
}
✅ 设计原则:当"值不存在"是正常的、预期的情况时,用 Option。当"值不存在"意味着错误或意外,用 Result。
当你的项目变复杂,io::Error 已经不够用了。你需要自己的错误类型——它能携带上下文、支持错误链、给用户清晰的反馈。
use std::fmt;
use std::error::Error;
use std::io;
#[derive(Debug)]
enum AppError {
Io(io::Error),
Config(String),
Database(DbError),
NotFound { resource: String, id: u64 },
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AppError::Io(e) => write!(f, "IO error: {e}"),
AppError::Config(msg) => write!(f, "Config error: {msg}"),
AppError::Database(e) => write!(f, "Database error: {e}"),
AppError::NotFound { resource, id } => {
write!(f, "{resource} with id {id} not found")
}
}
}
}
impl Error for AppError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
AppError::Io(e) => Some(e),
AppError::Database(e) => Some(e),
_ => None,
}
}
}
// 为每个子错误实现 From
impl From<io::Error> for AppError {
fn from(e: io::Error) -> Self { AppError::Io(e) }
}
impl From<DbError> for AppError {
fn from(e: DbError) -> Self { AppError::Database(e) }
}
use thiserror::Error;
#[derive(Error, Debug)]
enum AppError {
#[error("IO error: {0}")]
Io(#[from] io::Error), // auto From + source
#[error("Config error: {0}")]
Config(String),
#[error("Database error: {0}")]
Database(#[from] DbError), // auto From + source
#[error("{resource} with id {id} not found")]
NotFound { resource: String, id: u64 },
}
thiserror 为你自动生成了 Display、Error、From 实现。#[from] 同时做了两件事:生成 From impl 和设置 source()。
use error_set::error_set;
error_set! {
AppError = {
Io(std::io::Error),
Config(String),
Database(DbError),
NotFound { resource: String, id: u64 },
}
}
// error_set 自动生成:
// - Display / Error impls
// - From 转换
// - 错误子集(可以定义分层错误)
// - 比手写 thiserror 更少的样板
use std::error::Error;
fn print_error_chain(err: &dyn Error) {
let mut source = Some(err);
let mut depth = 0;
while let Some(e) = source {
println!("{}{}", " ".repeat(depth), e);
source = e.source();
depth += 1;
}
}
// 输出示例:
// Database error: connection refused
// Connection error: tcp connect failed
// IO error: Connection refused (os error 111)
✅ 库 crate 用 thiserror,应用 crate 用 anyhow——这是 Rust 社区的共识。库需要精确的错误类型供调用者匹配;应用只需要把错误一路传播到顶层。
anyhow 是应用代码的瑞士军刀——不需要定义错误类型,? 万物皆可传播,需要时还能加上下文。
use anyhow::{Context, Result, anyhow, bail};
fn read_config(path: &str) -> Result<Config> {
let content = fs::read_to_string(path)
.context(format!("Failed to read config from {path}"))?;
let config: Config = toml::from_str(&content)
.context("Failed to parse config")?;
if config.pool_size == 0 {
bail!("pool_size cannot be zero");
}
Ok(config)
}
// 顶层处理
fn main() -> anyhow::Result<()> {
let config = read_config("config.toml")?;
run_server(config)?;
Ok(())
}
// main 返回 anyhow::Result 时,错误会自动打印到 stderr
💡 context 的价值:原始错误只告诉你"什么技术故障发生了"。context 告诉你"在做什么时发生了"。两者结合才有完整的调试信息。
use anyhow::{Context, Result};
fn process_order(order_id: u64) -> Result<Order> {
let order = db::find_order(order_id)
.context("Failed to fetch order from database")?;
let user = db::find_user(order.user_id)
.context("Failed to fetch order's user")?;
let payment = payment::charge(&order, &user)
.context(format!(
"Payment failed for order {order_id}, user {}",
user.name
))?;
Ok(order)
}
// 错误输出示例:
// Payment failed for order 42, user Alice
// Caused by:
// 0: Charge request failed
// 1: Connection refused (os error 111)
// context() 立即构造字符串——即使成功也要花时间
.context(format!("Failed to process order {order_id}"))?
// with_context() 仅在失败时才执行闭包
.with_context(|| format!("Failed to process order {order_id}"))?
// 推荐在字符串构造有开销时使用 with_context
// 1. Cargo.toml
[dependencies]
anyhow = { version = "1", features = ["backtrace"] }
// 2. 设置环境变量
// RUST_BACKTRACE=1
// 3. 错误中自动包含 backtrace
fn deep_function() -> Result<()> {
Ok(fs::read_to_string("nonexistent")?)
}
// 错误输出包含完整调用栈:
// No such file or directory (os error 2)
// Stack backtrace:
// 0: anyhow::backtrace::capture::...
// 1: deep_function::...
// 2: main::...
// 什么时候用 anyhow?
// ✅ main 函数
// ✅ CLI 工具
// ✅ 应用逻辑(不需要
// 调用者匹配具体错误)
// ✅ 原型开发
fn main() -> anyhow::Result<()> {
let config = read_config()?;
run(config)?;
Ok(())
}
// 什么时候用 thiserror?
// ✅ 库 crate 的公开 API
// ✅ 调用者需要 match 错误
// ✅ 需要精确的错误类型
// ✅ 不同错误需要不同处理
#[derive(Error, Debug)]
pub enum DbError {
#[error("Connection failed: {0}")]
Connection(String),
#[error("Query failed: {0}")]
Query(String),
}
大型项目中,错误不是扁平的——它们有层次。每一层都应该有自己的错误类型,并在边界做转换。
// db-crate/src/error.rs
use thiserror::Error;
#[derive(Error, Debug)]
pub enum DbError {
#[error("Connection to {host}:{port} failed: {source}")]
ConnectionFailed {
host: String,
port: u16,
source: sqlx::Error,
},
#[error("Query timeout after {ms}ms: {query}")]
QueryTimeout { ms: u64, query: String },
#[error("Record not found: {table}/{id}")]
NotFound { table: String, id: String },
#[error("Unique constraint violated: {constraint}")]
UniqueViolation { constraint: String },
#[error(transparent)]
Internal(#[from] sqlx::Error),
}
// service-crate/src/error.rs
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ServiceError {
#[error("User {user_id} not found")]
UserNotFound { user_id: u64 },
#[error("Email already registered")]
DuplicateEmail,
#[error("Service temporarily unavailable")]
Unavailable,
#[error("Internal error")]
Internal, // 不暴露 DB 细节!
}
// 显式转换:DB 错误 → 服务错误
impl From<DbError> for ServiceError {
fn from(e: DbError) -> Self {
match e {
DbError::NotFound { table, id } if table == "users" => {
ServiceError::UserNotFound {
user_id: id.parse().unwrap_or(0),
}
}
DbError::UniqueViolation { ref constraint }
if constraint.contains("email") => {
ServiceError::DuplicateEmail
}
DbError::ConnectionFailed { .. }
| DbError::QueryTimeout { .. } => {
ServiceError::Unavailable
}
_ => ServiceError::Internal,
}
}
}
// api-crate/src/error.rs
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use serde::Serialize;
#[derive(Serialize)]
struct ErrorResponse {
error: ErrorBody,
}
#[derive(Serialize)]
struct ErrorBody {
code: String,
message: String,
#[serde(skip_serializing_if = "Option::is_none")]
request_id: Option<String>,
}
impl IntoResponse for ServiceError {
fn into_response(self) -> Response {
let (status, code, message) = match &self {
ServiceError::UserNotFound { .. } => (
StatusCode::NOT_FOUND,
"USER_NOT_FOUND",
"The requested user does not exist",
),
ServiceError::DuplicateEmail => (
StatusCode::CONFLICT,
"DUPLICATE_EMAIL",
"This email is already registered",
),
ServiceError::Unavailable => (
StatusCode::SERVICE_UNAVAILABLE,
"SERVICE_UNAVAILABLE",
"Service is temporarily unavailable",
),
ServiceError::Internal => (
StatusCode::INTERNAL_SERVER_ERROR,
"INTERNAL_ERROR",
"An internal error occurred",
),
};
// 日志中记录完整错误,响应中只返回安全信息
tracing::error!(error = %self, "Request failed");
(status, axum::Json(ErrorResponse {
error: ErrorBody {
code: code.to_string(),
message: message.to_string(),
request_id: None,
},
})).into_response()
}
}
🚨 安全原则:绝不把内部错误暴露给客户端!
#[error(transparent)] 在日志中保留完整链,但 API 响应必须脱敏异步 Rust 的错误处理有一些独特的坑——JoinError、timeout、cancellation 安全性。理解它们才能写出健壮的异步代码。
use tokio::task::JoinError;
// 1. 任务 panic
let handle = tokio::spawn(async {
panic!("task crashed!");
});
let result = handle.await; // Result<T, JoinError>
match result {
Ok(value) => println!("成功: {value:?}"),
Err(e) if e.is_panic() => {
// 任务内部 panic 了
// 注意:panic 信息被包装了
println!("任务 panic: {e}");
}
Err(e) if e.is_cancelled() => {
println!("任务被取消");
}
Err(e) => println!("Join 错误: {e}"),
}
// 2. 任务被取消(JoinHandle::abort)
handle.abort();
⚠️ tokio::spawn 要求 Send + 'static——意味着错误类型也必须满足。如果你的错误包含 !Send 类型(如 Rc),需要先转换为 Send 友好的形式。
use tokio::time::{timeout, Duration};
use anyhow::{Context, Result};
async fn fetch_with_timeout(url: &str) -> Result<String> {
timeout(Duration::from_secs(5), reqwest::get(url))
.await
.context("Request timed out after 5 seconds")? // ← ElapsedError
.context(format!("Failed to fetch {url}"))?
.text()
.await
.context("Failed to read response body")
}
// timeout 返回 Result<T, ElapsedError>
// ElapsedError 需要二次处理内层 Result
🚨 这是异步错误处理中最隐蔽的 bug 来源!当一个 Future 被 drop(如 timeout 触发、select! 切换),操作可能被半途放弃。
use tokio::select;
use tokio::sync::mpsc;
// ❌ 不安全:可能丢失消息
async fn process_unsafe(mut rx: mpsc::Receiver<Msg>) {
// 如果 select! 走了 timeout 分支,
// rx.recv() 返回的 Some(msg) 就丢了!
select! {
msg = rx.recv() => {
// 处理 msg
}
_ = tokio::time::sleep(Duration::from_secs(30)) => {
// 超时
}
}
}
// ✅ 安全:使用 cancellation-safe 模式
async fn process_safe(mut rx: mpsc::Receiver<Msg>) {
select! {
msg = rx.recv() => {
// recv() 是 cancellation-safe 的:
// 如果被 drop,消息留在 channel 中不丢失
}
_ = tokio::time::sleep(Duration::from_secs(30)) => {
// 超时
}
}
}
// ⚠️ 哪些操作是 cancellation-safe 的?
// ✅ recv() — 消息留在 channel
// ✅ read() — 数据留在 buffer
// ❌ send() — 消息可能已发送但未确认
// ❌ write() — 数据可能部分写入
use tokio::select;
async fn multi_source() -> Result<Data> {
let mut db_fut = db::fetch();
let mut cache_fut = cache::fetch();
let mut api_fut = api::fetch();
// 任一成功就返回,全部失败才报错
let mut errors = Vec::new();
loop {
select! {
result = &mut db_fut => {
match result {
Ok(data) => return Ok(data),
Err(e) => {
tracing::warn!("DB failed: {e}");
errors.push(e.into());
}
}
}
result = &mut cache_fut => {
match result {
Ok(data) => return Ok(data),
Err(e) => {
tracing::warn!("Cache failed: {e}");
errors.push(e.into());
}
}
}
result = &mut api_fut => {
match result {
Ok(data) => return Ok(data),
Err(e) => {
tracing::warn!("API failed: {e}");
errors.push(e.into());
}
}
}
}
if errors.len() == 3 {
anyhow::bail!("All sources failed: {:?}", errors);
}
}
}
理论够了,让我们从零构建一个完整的三层错误处理框架:数据库层 → 服务层 → API 层。
use sqlx::error::DatabaseError;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum DbError {
#[error("Connection pool exhausted")]
PoolExhausted,
#[error("Record not found in {table}: {key}={value}")]
NotFound {
table: String,
key: String,
value: String,
},
#[error("Duplicate entry in {table}: {constraint}")]
Duplicate {
table: String,
constraint: String,
},
#[error("Query timed out after {ms}ms")]
Timeout { ms: u64 },
#[error("Database operation failed")]
Operational(#[source] sqlx::Error),
}
impl From<sqlx::Error> for DbError {
fn from(e: sqlx::Error) -> Self {
match &e {
sqlx::Error::RowNotFound => DbError::Operational(e),
sqlx::Error::PoolTimedOut => DbError::PoolExhausted,
sqlx::Error::Database(db_err) => {
// 解析 PostgreSQL 特定错误码
// 23505 = unique_violation
if db_err.code().map(|c| c == "23505").unwrap_or(false) {
DbError::Duplicate {
table: db_err.table()
.unwrap_or("unknown")
.to_string(),
constraint: db_err.constraint()
.unwrap_or("unknown")
.to_string(),
}
} else {
DbError::Operational(e)
}
}
_ => DbError::Operational(e),
}
}
}
// 数据库仓库方法
impl UserRepo {
pub async fn find_by_id(&self, id: u64) -> Result<UserRow, DbError> {
sqlx::query_as!(
UserRow,
"SELECT * FROM users WHERE id = $1",
id as i64
)
.fetch_one(&self.pool)
.await
.map_err(|e| match e {
sqlx::Error::RowNotFound => DbError::NotFound {
table: "users".into(),
key: "id".into(),
value: id.to_string(),
},
other => other.into(),
})
}
pub async fn create(&self, user: NewUser) -> Result<UserRow, DbError> {
sqlx::query_as!(
UserRow,
"INSERT INTO users (email, name) VALUES ($1, $2)
RETURNING *",
user.email, user.name
)
.fetch_one(&self.pool)
.await
.map_err(Into::into) // sqlx::Error → DbError
}
}
use thiserror::Error;
use crate::db::DbError;
#[derive(Error, Debug)]
pub enum ServiceError {
// 业务领域错误——用领域语言表达
#[error("User {id} not found")]
UserNotFound { id: u64 },
#[error("Email {email} already registered")]
DuplicateEmail { email: String },
#[error("Invalid email format: {email}")]
InvalidEmail { email: String },
#[error("Cannot delete user {id}: has active orders")]
UserHasOrders { id: u64 },
// 基础设施故障——对业务屏蔽细节
#[error("Service temporarily unavailable")]
Unavailable,
#[error("Internal error")]
Internal,
}
// 关键:显式转换,不泄露 DB 细节
impl From<DbError> for ServiceError {
fn from(e: DbError) -> Self {
match e {
DbError::NotFound { table, value, .. }
if table == "users" =>
{
ServiceError::UserNotFound {
id: value.parse().unwrap_or(0),
}
}
DbError::Duplicate { constraint, .. }
if constraint.contains("email") =>
{
// 我们没有 email 信息——需要调用者提供
ServiceError::DuplicateEmail {
email: "unknown".into(),
}
}
DbError::PoolExhausted | DbError::Timeout { .. } => {
ServiceError::Unavailable
}
_ => ServiceError::Internal,
}
}
}
// 服务实现
impl UserService {
pub async fn get_user(&self, id: u64) -> Result<User, ServiceError> {
let row = self.repo.find_by_id(id).await?;
Ok(User::from(row))
}
pub async fn create_user(
&self,
email: String,
name: String,
) -> Result<User, ServiceError> {
// 业务验证
if !email.contains('@') {
return Err(ServiceError::InvalidEmail { email });
}
let row = self.repo
.create(NewUser { email: email.clone(), name })
.await
.map_err(|e| match e {
DbError::Duplicate { constraint, .. }
if constraint.contains("email") =>
{
ServiceError::DuplicateEmail { email }
}
other => other.into(),
})?;
Ok(User::from(row))
}
}
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
Json,
};
use serde::Serialize;
use service::ServiceError;
use uuid::Uuid;
#[derive(Debug)]
pub struct ApiError {
inner: ServiceError,
request_id: Uuid,
}
impl ApiError {
pub fn new(inner: ServiceError, request_id: Uuid) -> Self {
Self { inner, request_id }
}
}
#[derive(Serialize)]
struct ErrorBody {
code: String,
message: String,
request_id: String,
}
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
let (status, code, message) = match &self.inner {
ServiceError::UserNotFound { .. } => (
StatusCode::NOT_FOUND,
"USER_NOT_FOUND",
"The requested user does not exist",
),
ServiceError::DuplicateEmail { .. } => (
StatusCode::CONFLICT,
"DUPLICATE_EMAIL",
"This email address is already registered",
),
ServiceError::InvalidEmail { .. } => (
StatusCode::BAD_REQUEST,
"INVALID_EMAIL",
"Please provide a valid email address",
),
ServiceError::UserHasOrders { .. } => (
StatusCode::CONFLICT,
"USER_HAS_ORDERS",
"Cannot delete user with active orders",
),
ServiceError::Unavailable => (
StatusCode::SERVICE_UNAVAILABLE,
"SERVICE_UNAVAILABLE",
"Please try again later",
),
ServiceError::Internal => (
StatusCode::INTERNAL_SERVER_ERROR,
"INTERNAL_ERROR",
"An unexpected error occurred",
),
};
// 📝 日志中记录完整信息(包括 source chain)
tracing::error!(
error = %self.inner,
error_chain = ?self.inner,
request_id = %self.request_id,
status = %status.as_u16(),
"Request failed"
);
// 🔒 响应中只返回安全信息
(status, Json(ErrorBody {
code: code.to_string(),
message: message.to_string(),
request_id: self.request_id.to_string(),
})).into_response()
}
}
// Handler 中使用
async fn get_user(
State(state): State<AppState>,
Path(id): Path<u64>,
) -> Result<Json<User>, ApiError> {
let request_id = Uuid::new_v4();
state.service
.get_user(id)
.await
.map_err(|e| ApiError::new(e, request_id))
.map(Json)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn db_not_found_to_service_user_not_found() {
let db_err = DbError::NotFound {
table: "users".into(),
key: "id".into(),
value: "42".into(),
};
let svc_err: ServiceError = db_err.into();
assert!(matches!(
svc_err,
ServiceError::UserNotFound { id: 42 }
));
}
#[test]
fn db_duplicate_email_to_service() {
let db_err = DbError::Duplicate {
table: "users".into(),
constraint: "users_email_key".into(),
};
let svc_err: ServiceError = db_err.into();
assert!(matches!(svc_err, ServiceError::DuplicateEmail { .. }));
}
#[test]
fn db_pool_exhausted_to_unavailable() {
let db_err = DbError::PoolExhausted;
let svc_err: ServiceError = db_err.into();
assert!(matches!(svc_err, ServiceError::Unavailable));
}
#[tokio::test]
async fn api_error_response_format() {
let svc_err = ServiceError::UserNotFound { id: 42 };
let api_err = ApiError::new(svc_err, Uuid::nil());
let response = api_err.into_response();
assert_eq!(response.status(), 404);
}
}
| 场景 | 推荐做法 | crate |
|---|---|---|
| 库公开 API | 自定义 enum + thiserror | thiserror |
| 应用 main | anyhow::Result | anyhow |
| CLI 工具 | anyhow + context | anyhow |
| 跨层错误 | 显式 From 转换 | std |
| HTTP API | ServiceError → HTTP status + JSON | axum |
| 异步任务 | JoinError 处理 + cancellation safety | tokio |
| 超时控制 | tokio::time::timeout + context | tokio |
| 错误链追踪 | Error::source() + #[from] | std |