错误处理
大约 5 分钟
统一错误处理系统 (Error
和 Result<T>
)
为了构建一个健壮、可维护且对开发者友好的应用,我们建立了一套统一的错误处理系统。该系统的核心是自定义的 enum Error
和全局的 type Result<T> = std::result::Result<T, Error>
。
这套系统的目标是:
- 统一错误类型:将来自不同库(数据库、Redis、JSON 解析等)的错误全部转换为我们自己的
Error
类型。 - 简化错误传递:利用 Rust 的
?
操作符,实现错误的轻松向上传播。 - 自动转换为 API 响应:与
ApiResponse
模块无缝集成,自动将Error
转换为格式正确、状态码恰当的 HTTP 响应。
核心工作流
- 在 Service 层,所有可能失败的函数都返回
Result<T>
。 - 使用
?
操作符处理来自底层库的错误,它们会自动转换为我们的Error
类型。 - 在 Handler 层,调用 Service 函数并将其返回的
Result<T>
传递给ApiResponse::from_result()
。 - 系统自动完成剩下的所有工作!
核心定义:enum Error
我们的自定义错误类型非常简洁,只有两种变体:
#[derive(Debug)]
#[non_exhaustive]
pub enum Error {
// 用于包装来自底层库的、未指定 HTTP 状态的通用错误
Message(String),
// 用于业务逻辑中需要明确指定 HTTP 状态码的错误
WithStatus(StatusCode, String),
}
Error::Message(String)
- 这是“默认”的错误类型。
- 当我们使用
?
将一个外部库的错误(如sea_orm::DbErr
)转换为Error
时,它就会被包装成这个变体。 - 当它最终被转换为 API 响应时,默认会使用
500 Internal Server Error
状态码,因为我们认为这是一个未被业务逻辑明确处理的内部错误。
Error::WithStatus(StatusCode, String)
- 这是我们在业务逻辑中 主动创建 的错误。
- 它允许我们精确地控制返回给客户端的 HTTP 状态码和错误消息。
- 例如,当用户查询一个不存在的资源时,我们应该创建一个
Error::WithStatus(StatusCode::NOT_FOUND, ...)
。
实战案例:从 Service 到 Handler 的错误处理流程
让我们通过一个真实的“编辑用户信息”的例子,看看这套系统是如何工作的。
1. Service 层:业务逻辑与错误处理
在 Service 层,我们专注于实现业务逻辑。函数签名明确返回 Result<T>
,并利用 ?
和 Error
构造器来处理各种可能失败的情况。
// service/sys_user_service.rs
pub async fn edit(arg: SysUserEdit) -> Result<SysUserModel> {
let db = DB().await;
// 1. 查找用户,如果数据库操作失败,`?` 会将 DbErr 转换为 Error::Message
let rmodel = sys_user::Entity::find_by_id(arg.id).one(db).await?;
// 2. 检查用户是否存在,如果不存在,我们创建一个业务特定的错误
let model = rmodel.ok_or_else(|| Error::not_found(format!("ID 为 {} 的用户不存在", arg.id)))?;
// - .ok_or_else() 是一个优雅的将 Option 转换为 Result 的方法。
// - 我们主动创建了一个 Error::not_found,它会被转换为 404 HTTP 状态码。
// 3. 准备更新数据
let mut amodel: sys_user::ActiveModel = model.into();
amodel.user_name = Set(arg.user_name);
// ... 其他字段 ...
amodel.remark = Set(arg.remark);
// 4. 执行更新,如果失败,`?` 会再次自动转换错误
let umodel = amodel.update(db).await?;
Ok(umodel)
}
这段代码的亮点:
- 简洁性: 整个函数没有一个
match
或if let Err(...)
块,代码流畅易读。 - 精确性: 我们区分了两种错误:
- 数据库错误 (
DbErr
):由?
自动处理,它们是不可预期的内部错误。 - 业务规则错误 (
用户不存在
):由我们主动创建,是可预期的、需要明确告知客户端的错误。
- 数据库错误 (
2. Handler 层:协调与响应生成
Handler 层现在变得非常“薄”,它的职责就是调用 Service 并将结果交给 ApiResponse
。
// handler/sys_user_handler.rs
pub async fn edit(VJson(arg): VJson<SysUserEdit>) -> Response {
// 整个业务流程被封装在一个 Service 函数中,包含事务保证数据一致性
// 假设我们有一个包含了所有更新步骤的 service 函数
let result = sys_user_service::update_user_full_info(arg).await;
// 核心:只需这一行!
// ApiResponse::from_result 会智能地处理一切
ApiResponse::from_result(result)
}
ApiResponse::from_result(result)
会做什么?
- 如果
result
是Ok(data)
: 它会生成一个200 OK
的成功响应,并将data
序列化为 JSON。 - 如果
result
是Err(Error::not_found("..."))
: 它会生成一个404 Not Found
的错误响应。 - 如果
result
是Err(Error::Message("database timeout"))
: 它会生成一个500 Internal Server Error
的错误响应。
这完美地展示了我们的分层架构:Service 负责定义“是什么错误”,而 ApiResponse
负责决定“如何向客户端展示这个错误”。Handler 只需充当桥梁。
实现原理揭秘
impl_error_from!
宏:错误的“适配器工厂”
我们不可能为几十种错误类型手动编写 impl From<T> for Error
。因此,我们创建了一个宏来自动化这个过程。
macro_rules! impl_error_from {
($($error_type:ty),* $(,)?) => {
$(
impl From<$error_type> for Error {
fn from(err: $error_type) -> Self {
Error::Message(err.to_string())
}
}
)*
};
}
impl_error_from!(
sea_orm::DbErr,
redis::RedisError,
// ...
);
这个宏是 ?
能够自动转换错误的关键。
IntoResponse
Trait:与 Axum 的桥梁
Error
类型实现了 Axum 的 IntoResponse
Trait,这使得它能被 ApiResponse
进一步处理成最终的 Response
。
impl IntoResponse for Error {
fn into_response(self) -> Response {
match self {
Error::Message(msg) => ApiResponse::internal_server_error(msg),
Error::WithStatus(status, msg) => ApiResponse::custom(status, msg),
}
}
}
总结与最佳实践
重要规范
Service 层:
- 必须 返回
Result<T>
。 - 使用
?
来传播不可预期的底层错误。 - 使用
Error::not_found()
、Error::bad_request()
等辅助函数来创建明确的业务逻辑错误。 - 使用
.ok_or_else(|| Error::not_found(...)))?
将Option
转换为带有业务含义的Result
。
- 必须 返回
Handler 层:
- 必须 返回
Response
(或impl IntoResponse
)。 - 应该 使用
ApiResponse::from_result()
来处理来自 Service 层的Result
。 - Handler 自身 不应该 包含
match
或if let Err
这样的错误判断逻辑。
- 必须 返回