目录
AI提问1:Rust代码(impl From<DbErr> for AppError)的作用,请举例详细说明:
impl From<DbErr> for AppError {
fn from(value: DbErr) -> Self {
Self::Internal(value.to_string())
}
}
如果有其他错误类型,如 :ApiError ,如何处理呢 ?
AI回复1:
这段代码的核心作用是:
把
DbErr类型的数据库错误,自动转换成项目统一的AppError类型。
impl From<DbErr> for AppError {
fn from(value: DbErr) -> Self {
Self::Internal(value.to_string())
}
}
一、先看完整的上下文
假设你的项目中定义了统一错误类型:
#[derive(Debug)]
pub enum AppError {
/// 系统内部错误
Internal(String),
/// 参数校验错误
Validation(String),
/// 数据不存在
NotFound(String),
}
SeaORM 的数据库操作可能返回:
Result<User, DbErr>
但是在业务层或控制器层,你希望统一返回:
Result<User, AppError>
这时就需要把 DbErr 转换为 AppError。
二、逐行解释
impl From<DbErr> for AppError {
表示:
为
AppError实现From<DbErr>trait,让AppError可以从DbErr转换而来。
可以理解为建立了一条转换规则:
DbErr → AppError
这里:
DbErr:源错误类型AppError:目标错误类型
fn from(value: DbErr) -> Self {
value 是需要被转换的数据库错误。
Self 代表当前实现的目标类型,也就是:
AppError
因此,这行代码等价于:
fn from(value: DbErr) -> AppError {
Self::Internal(value.to_string())
等价于:
AppError::Internal(value.to_string())
它做了两件事:
- 使用
value.to_string()将DbErr转换成字符串; - 将错误字符串包装成
AppError::Internal。
例如,数据库错误可能是:
Connection Error: Connection refused
转换后变成:
AppError::Internal(
"Connection Error: Connection refused".to_string()
)
三、最直接的使用方式:.into()
实现 From<DbErr> for AppError 后,就可以使用 .into() 进行转换。
fn convert_error(db_err: DbErr) -> AppError {
db_err.into()
}
虽然代码调用的是:
db_err.into()
但实际上执行的是:
AppError::from(db_err)
下面两种写法完全等价:
let app_error = AppError::from(db_err);
let app_error: AppError = db_err.into();
原因是 Rust 存在以下关联关系:
实现了
From<A> for B后,Rust 会自动提供Into<B> for A。
也就是说:
impl From<DbErr> for AppError
会让下面的转换成立:
DbErr::into() -> AppError
四、最重要的用途:配合 ? 运算符
这段代码最常见、最有价值的用途,是让数据库错误能够通过 ? 自动转换。
假设有一个数据库查询函数:
async fn find_user_from_db() -> Result<User, DbErr> {
// 数据库查询
}
业务函数要求返回统一错误:
async fn find_user() -> Result<User, AppError> {
let user = find_user_from_db().await?;
Ok(user)
}
这里需要特别注意:
find_user_from_db().await
返回的是:
Result<User, DbErr>
但是当前函数返回的是:
Result<User, AppError>
正常来说,错误类型不同:
DbErr != AppError
为什么还能使用 ??
因为你实现了:
impl From<DbErr> for AppError
所以 ? 会自动完成转换。它的逻辑大致等价于:
async fn find_user() -> Result<User, AppError> {
let result = find_user_from_db().await;
let user = match result {
Ok(user) => user,
Err(db_err) => {
return Err(AppError::from(db_err));
}
};
Ok(user)
}
也就是说:
let user = find_user_from_db().await?;
在发生错误时,相当于执行:
return Err(AppError::from(db_err));
五、SeaORM 中的实际案例
例如使用 SeaORM 查询用户:
use sea_orm::{
ColumnTrait, DatabaseConnection, DbErr, EntityTrait, QueryFilter,
};
pub async fn find_user_by_id(
db: &DatabaseConnection,
user_id: i64,
) -> Result<Option<user::Model>, AppError> {
let user = user::Entity::find()
.filter(user::Column::Id.eq(user_id))
.one(db)
.await?;
Ok(user)
}
SeaORM 的:
.one(db).await
返回:
Result<Option<user::Model>, DbErr>
而当前函数要求返回:
Result<Option<user::Model>, AppError>
由于已经实现:
impl From<DbErr> for AppError
所以这里可以直接写:
.await?;
不需要每次都手动写:
.await .map_err(|error| AppError::Internal(error.to_string()))?;
没有实现 From 时
你只能手动转换:
let user = user::Entity::find()
.filter(user::Column::Id.eq(user_id))
.one(db)
.await
.map_err(|error| AppError::Internal(error.to_string()))?;
实现 From 后
可以简化成:
let user = user::Entity::find()
.filter(user::Column::Id.eq(user_id))
.one(db)
.await?;
因此,From 的主要价值是:
- 统一项目错误类型;
- 简化
?的使用; - 避免重复写
map_err; - 降低业务代码与第三方错误类型的耦合。
六、如果还有 ApiError,应该如何处理?
如果项目中还有其他错误类型,例如:
#[derive(Debug)]
pub struct ApiError {
pub code: i32,
pub message: String,
}
可以继续为 AppError 实现:
impl From<ApiError> for AppError {
fn from(value: ApiError) -> Self {
Self::Internal(value.message)
}
}
此时就存在两条转换规则:
DbErr → AppError ApiError → AppError
完整代码如下:
impl From<DbErr> for AppError {
fn from(value: DbErr) -> Self {
AppError::Internal(value.to_string())
}
}
impl From<ApiError> for AppError {
fn from(value: ApiError) -> Self {
AppError::Internal(value.message)
}
}
业务函数中可以同时处理数据库错误和 API 错误:
async fn process_user() -> Result<User, AppError> {
// 这里可能返回 DbErr
let user = find_user_from_db().await?;
// 这里可能返回 ApiError
call_external_api().await?;
Ok(user)
}
发生数据库错误时:
DbErr → AppError
发生 API 错误时:
ApiError → AppError
? 会根据实际错误类型,寻找对应的 From 实现。
七、更推荐的设计:不同错误使用不同枚举成员
不建议把所有错误都转换成:
AppError::Internal(String)
因为这样会丢失错误类别。
例如:
#[derive(Debug)]
pub enum AppError {
/// 数据库错误
Database(String),
/// 第三方接口错误
Api(String),
/// 参数校验错误
Validation(String),
/// 数据不存在
NotFound(String),
/// 未知内部错误
Internal(String),
}
然后分别实现:
impl From<DbErr> for AppError {
fn from(value: DbErr) -> Self {
Self::Database(value.to_string())
}
}
impl From<ApiError> for AppError {
fn from(value: ApiError) -> Self {
Self::Api(value.message)
}
}
这样就能保留错误分类。
例如:
async fn process() -> Result<(), AppError> {
database_operation().await?;
api_operation().await?;
Ok(())
}
数据库错误会转换成:
AppError::Database("数据库连接失败".to_string())
API 错误会转换成:
AppError::Api("第三方服务请求超时".to_string())
后续在控制器层,可以根据错误类型返回不同的状态码。
八、完整示例
下面使用简化版错误类型演示整个流程。
use std::fmt;
// 模拟数据库错误
#[derive(Debug)]
pub struct DbErr {
pub message: String,
}
impl fmt::Display for DbErr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "数据库错误:{}", self.message)
}
}
// 模拟第三方 API 错误
#[derive(Debug)]
pub struct ApiError {
pub code: i32,
pub message: String,
}
// 项目统一错误
#[derive(Debug)]
pub enum AppError {
Database(String),
Api {
code: i32,
message: String,
},
Internal(String),
}
// DbErr 转 AppError
impl From<DbErr> for AppError {
fn from(value: DbErr) -> Self {
Self::Database(value.to_string())
}
}
// ApiError 转 AppError
impl From<ApiError> for AppError {
fn from(value: ApiError) -> Self {
Self::Api {
code: value.code,
message: value.message,
}
}
}
模拟数据库操作:
fn query_database() -> Result<String, DbErr> {
Err(DbErr {
message: "无法连接 MySQL".to_string(),
})
}
模拟 API 操作:
fn call_api() -> Result<String, ApiError> {
Err(ApiError {
code: 50001,
message: "第三方接口请求超时".to_string(),
})
}
在业务层统一处理:
fn execute_database_service() -> Result<String, AppError> {
let result = query_database()?;
Ok(result)
}
fn execute_api_service() -> Result<String, AppError> {
let result = call_api()?;
Ok(result)
}
这里:
query_database()?;
会自动执行:
AppError::from(db_error)
而:
call_api()?;
会自动执行:
AppError::from(api_error)
九、可以使用 thiserror 进一步简化
在实际 Rust Web 项目中,通常会使用 thiserror 定义错误。
use sea_orm::DbErr;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum AppError {
#[error("数据库操作失败:{0}")]
Database(#[from] DbErr),
#[error("API 调用失败:{0}")]
Api(#[from] ApiError),
#[error("参数校验失败:{0}")]
Validation(String),
#[error("数据不存在:{0}")]
NotFound(String),
#[error("系统内部错误:{0}")]
Internal(String),
}
这里的:
#[from]
会自动生成对应的 From 实现。
例如:
Database(#[from] DbErr)
相当于自动生成:
impl From<DbErr> for AppError {
fn from(value: DbErr) -> Self {
AppError::Database(value)
}
}
而:
Api(#[from] ApiError)
相当于自动生成:
impl From<ApiError> for AppError {
fn from(value: ApiError) -> Self {
AppError::Api(value)
}
}
十、保留原始错误还是转换成字符串?
你目前的写法是:
Self::Internal(value.to_string())
这会把原始的 DbErr 转换成字符串。优点是简单,但缺点是:
- 丢失原始错误类型;
- 后续无法判断具体是哪一种
DbErr; - 错误链信息可能不完整;
- 不方便通过
source()追踪原始错误。
当前写法
pub enum AppError {
Internal(String),
}
impl From<DbErr> for AppError {
fn from(value: DbErr) -> Self {
Self::Internal(value.to_string())
}
}
适合简单项目。
更推荐的写法
直接保存原始错误:
use sea_orm::DbErr;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum AppError {
#[error("数据库操作失败:{0}")]
Database(#[from] DbErr),
#[error("系统内部错误:{0}")]
Internal(String),
}
此时 AppError 内部保存的是完整的:
DbErr
而不是一段普通字符串。
十一、ApiError 到底应该转换到哪里?
这取决于 ApiError 的定位。
情况一:ApiError 是第三方接口调用错误
例如调用短信接口、邮件接口、支付接口:
pub struct ApiError {
pub code: i32,
pub message: String,
}
那么应该:
impl From<ApiError> for AppError {
fn from(value: ApiError) -> Self {
AppError::Api {
code: value.code,
message: value.message,
}
}
}
转换方向是:
ApiError → AppError
情况二:ApiError 是返回给前端的 HTTP 错误
例如:
pub struct ApiError {
pub status: u16,
pub code: i32,
pub message: String,
}
这时更合理的方向可能是:
AppError → ApiError
也就是业务层先统一产生 AppError,到控制器或响应层再转换成 ApiError。
impl From<AppError> for ApiError {
fn from(value: AppError) -> Self {
match value {
AppError::Database(error) => Self {
status: 500,
code: 50001,
message: "数据库操作失败".to_string(),
},
AppError::Validation(message) => Self {
status: 400,
code: 40001,
message,
},
AppError::NotFound(message) => Self {
status: 404,
code: 40401,
message,
},
AppError::Api { message, .. } => Self {
status: 502,
code: 50201,
message,
},
AppError::Internal(message) => Self {
status: 500,
code: 50000,
message,
},
}
}
}
这种架构通常更清晰:
SeaORM DbErr
↓
第三方 ApiError
↓
统一转换为 AppError
↓
控制器转换成 HTTP 响应
也就是:
基础设施错误 → 业务统一错误 → HTTP API 响应
十二、推荐的分层错误设计
对于你的 Rust Web 多模块项目,可以按照下面的方式设计:
数据库层: Result<T, DbErr> 第三方接口层: Result<T, ApiClientError> 业务逻辑层: Result<T, AppError> 控制器层: AppError → HTTP Response
推荐定义:
#[derive(Debug, thiserror::Error)]
pub enum AppError {
#[error("数据库操作失败:{0}")]
Database(#[from] sea_orm::DbErr),
#[error("第三方接口调用失败:{0}")]
ExternalApi(#[from] ApiClientError),
#[error("参数错误:{0}")]
Validation(String),
#[error("数据不存在:{0}")]
NotFound(String),
#[error("没有操作权限:{0}")]
Forbidden(String),
#[error("用户未登录:{0}")]
Unauthorized(String),
#[error("系统内部错误:{0}")]
Internal(String),
}
这样业务代码就可以统一使用:
pub async fn create_user() -> Result<User, AppError> {
validate_user_input()?;
let remote_data = call_external_api().await?;
let user = save_user_to_database(remote_data).await?;
Ok(user)
}
不管底层产生:
DbErrApiClientError- 参数校验错误
- 权限错误
最终都会统一为:
AppError
十三、总结
你的代码:
impl From<DbErr> for AppError {
fn from(value: DbErr) -> Self {
Self::Internal(value.to_string())
}
}
主要有三个作用:
- 建立
DbErr → AppError的转换规则; - 允许使用
db_err.into()转换错误; - 允许在返回
Result<T, AppError>的函数中,对Result<T, DbErr>使用?。
存在其他错误类型时,可以继续实现:
impl From<ApiError> for AppError
但是要注意 ApiError 的职责:
- 如果它是第三方服务错误:
ApiError → AppError - 如果它是给前端返回的 HTTP 错误:通常是
AppError → ApiError
在正式项目中,更推荐直接保留原始错误:
#[derive(Debug, thiserror::Error)]
pub enum AppError {
#[error("数据库错误:{0}")]
Database(#[from] DbErr),
#[error("第三方接口错误:{0}")]
Api(#[from] ApiError),
}
而不是把所有错误都提前转换成 String。
AI提问2:字段属性#[from],会让 DbErr 自动转 AppError 吗 ?
#[derive(Debug, thiserror::Error)]
pub enum AppError {
#[error("数据库错误:{0}")]
Database(#[from] DbErr),
#[error("第三方接口错误:{0}")]
Api(#[from] ApiError),
}
AI回答2:
会。
这里的 #[from] 会让 thiserror 自动为你生成对应的 From 实现:
#[derive(Debug, thiserror::Error)]
pub enum AppError {
#[error("数据库错误:{0}")]
Database(#[from] DbErr),
#[error("第三方接口错误:{0}")]
Api(#[from] ApiError),
}
大致相当于你手动编写:
impl From<DbErr> for AppError {
fn from(error: DbErr) -> Self {
AppError::Database(error)
}
}
impl From<ApiError> for AppError {
fn from(error: ApiError) -> Self {
AppError::Api(error)
}
}
也就是说,建立了两条转换规则:
DbErr → AppError::Database(DbErr) ApiError → AppError::Api(ApiError)
thiserror 官方文档明确说明:包含 #[from] 的错误字段会自动生成对应的 From 实现,并且 #[from] 同时隐含了 #[source]。(Docs.rs)
1. 可以直接使用 AppError::from()
let db_error: DbErr = /* 数据库错误 */; let app_error = AppError::from(db_error);
转换后的结果是:
AppError::Database(db_error)
2. 可以使用 .into()
因为有了:
impl From<DbErr> for AppError
所以也可以反向使用 Into:
let db_error: DbErr = /* 数据库错误 */; let app_error: AppError = db_error.into();
它等价于:
let app_error = AppError::from(db_error);
3. 最常见的用途:配合 ?
假设 SeaORM 查询返回:
Result<Option<user::Model>, DbErr>
而你的业务方法返回:
Result<Option<user::Model>, AppError>
代码可以直接写:
use sea_orm::{DatabaseConnection, EntityTrait};
pub async fn find_user(
db: &DatabaseConnection,
user_id: i64,
) -> Result<Option<user::Model>, AppError> {
let user = user::Entity::find_by_id(user_id)
.one(db)
.await?;
Ok(user)
}
这里:
.one(db).await
返回的是:
Result<Option<user::Model>, DbErr>
但当前函数要求返回:
Result<Option<user::Model>, AppError>
当数据库操作失败时,? 大致会执行:
return Err(AppError::from(db_error));
最终得到:
AppError::Database(db_error)
因此下面这段简洁代码:
let user = user::Entity::find_by_id(user_id)
.one(db)
.await?;
大致等价于:
let user = match user::Entity::find_by_id(user_id)
.one(db)
.await
{
Ok(user) => user,
Err(db_error) => {
return Err(AppError::Database(db_error));
}
};
4. ApiError 同样可以自动转换
假设你定义了第三方接口错误:
#[derive(Debug, thiserror::Error)]
pub enum ApiError {
#[error("请求超时")]
Timeout,
#[error("接口返回错误:{0}")]
Response(String),
#[error("网络连接失败:{0}")]
Network(String),
}
统一错误:
use sea_orm::DbErr;
#[derive(Debug, thiserror::Error)]
pub enum AppError {
#[error("数据库错误:{0}")]
Database(#[from] DbErr),
#[error("第三方接口错误:{0}")]
Api(#[from] ApiError),
}
模拟第三方 API 调用:
async fn call_external_api() -> Result<String, ApiError> {
Err(ApiError::Timeout)
}
业务方法:
async fn execute_service() -> Result<String, AppError> {
let result = call_external_api().await?;
Ok(result)
}
当 call_external_api() 返回:
Err(ApiError::Timeout)
? 会自动把它转换为:
Err(AppError::Api(ApiError::Timeout))
5. 同一个函数可以同时处理两种错误
pub async fn create_user(
db: &DatabaseConnection,
) -> Result<(), AppError> {
// 可能产生 ApiError
let api_data = call_external_api().await?;
// 可能产生 DbErr
save_user_to_database(db, api_data).await?;
Ok(())
}
转换过程分别是:
call_external_api()
↓
ApiError
↓ #[from]
AppError::Api(ApiError)
以及:
save_user_to_database()
↓
DbErr
↓ #[from]
AppError::Database(DbErr)
业务方法最终只需要统一返回:
Result<(), AppError>
6. 它不会在任何场景下“主动转换”
需要注意,“自动转换”不是说 DbErr 一产生就立即变成 AppError。
只有代码需要把它转换成 AppError 时,转换才会发生,例如:
AppError::from(db_error)
let error: AppError = db_error.into();
或者:
database_operation().await?;
并且当前函数的返回错误类型是:
AppError
例如下面的代码不会发生转换:
async fn query() -> Result<(), DbErr> {
database_operation().await?;
Ok(())
}
因为当前函数本身返回的就是 DbErr,不需要转换。
而下面会发生转换:
async fn query() -> Result<(), AppError> {
database_operation().await?;
Ok(())
}
因为当前函数要求返回 AppError。
7. #[from] 还会保留原始错误
这种写法:
Database(#[from] DbErr)
会把原始的 DbErr 保存在 AppError 中。
也就是:
AppError::Database(db_error)
相比下面这种写法:
impl From<DbErr> for AppError {
fn from(value: DbErr) -> Self {
AppError::Internal(value.to_string())
}
}
使用 #[from] 保存原始错误通常更好,因为它没有提前把错误降级成普通字符串,并且 #[from] 还隐含 #[source],可以通过标准错误链追踪底层错误。(Docs.rs)
8. 对 ApiError 的要求
由于:
Api(#[from] ApiError)
中的 #[from] 同时意味着它是底层错误来源,因此 ApiError 通常需要实现:
std::error::Error
最简单的方式也是使用 thiserror:
#[derive(Debug, thiserror::Error)]
pub enum ApiError {
#[error("请求超时")]
Timeout,
#[error("服务器返回错误:{0}")]
Server(String),
}
这样它会自动实现:
DebugDisplaystd::error::Error
然后才能顺利作为 AppError 的底层错误来源。thiserror 文档说明,作为 source 的类型需要实现 std::error::Error,而 #[from] 自动隐含 #[source]。(Docs.rs)
9. 一个容易混淆的限制
使用 #[from] 的枚举成员,通常不能再随意加入其他业务字段。
下面可以:
#[error("第三方接口错误:{0}")]
Api(#[from] ApiError),
下面通常不可以:
#[error("第三方接口错误:{source}")]
Api {
#[from]
source: ApiError,
api_name: String,
}
因为使用 #[from] 的变体,除错误来源以及可选的回溯字段外,不能再包含普通附加字段。(Docs.rs)
需要附加 api_name 时,可以手动实现:
#[derive(Debug, thiserror::Error)]
pub enum AppError {
#[error("第三方接口 {api_name} 调用失败:{source}")]
Api {
api_name: String,
#[source]
source: ApiError,
},
}
然后手动构造:
let error = AppError::Api {
api_name: "短信服务".to_string(),
source: api_error,
};
总结
这段代码:
#[derive(Debug, thiserror::Error)]
pub enum AppError {
#[error("数据库错误:{0}")]
Database(#[from] DbErr),
#[error("第三方接口错误:{0}")]
Api(#[from] ApiError),
}
会自动生成:
impl From<DbErr> for AppError
和:
impl From<ApiError> for AppError
所以在返回 Result<T, AppError> 的方法中可以直接写:
database_operation().await?; api_operation().await?;
对应的自动转换结果是:
DbErr → AppError::Database(DbErr) ApiError → AppError::Api(ApiError)
这正是 thiserror 中 #[from] 最主要的作用。
AI提问3:有些地方会出现 #[source],这个是啥意思,有什么作用,举例说明,它跟 #[from] 有啥区别?
AI回复3:
#[source] 的核心作用是:
告诉 Rust:当前错误是由字段中的另一个底层错误引起的。
它主要用于建立“错误原因链”,让程序可以从上层错误继续追踪到真正的底层错误。
例如:
保存用户失败
└── 数据库操作失败
└── MySQL 连接超时
在 thiserror 中,标记了 #[source] 的字段会被 std::error::Error::source() 返回;字段名本身叫 source 时,即使省略 #[source],thiserror 也会自动将其识别为错误来源。(Docs.rs)
一、#[source] 的基本用法
假设底层有一个数据库错误:
use thiserror::Error;
#[derive(Debug, Error)]
pub enum DbErr {
#[error("数据库连接超时")]
ConnectionTimeout,
#[error("SQL 执行失败:{0}")]
Sql(String),
}
项目统一错误定义为:
#[derive(Debug, Error)]
pub enum AppError {
#[error("数据库操作失败")]
Database {
#[source]
error: DbErr,
},
}
这里:
#[source] error: DbErr
表示:
AppError::Database是上层错误,字段error中保存的DbErr是导致它发生的底层错误。
构造错误:
let error = AppError::Database {
error: DbErr::ConnectionTimeout,
};
此时有两层错误:
上层错误:数据库操作失败 底层原因:数据库连接超时
二、#[source] 实际生成了什么?
下面这段代码:
#[derive(Debug, Error)]
pub enum AppError {
#[error("数据库操作失败")]
Database {
#[source]
error: DbErr,
},
}
可以理解为 thiserror 帮你实现了类似逻辑:
impl std::error::Error for AppError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
AppError::Database { error } => Some(error),
}
}
}
因此可以通过标准库的 source() 获取底层错误:
use std::error::Error;
fn main() {
let error = AppError::Database {
error: DbErr::ConnectionTimeout,
};
println!("当前错误:{error}");
if let Some(source) = error.source() {
println!("底层原因:{source}");
}
}
输出大致为:
当前错误:数据库操作失败 底层原因:数据库连接超时
Rust 标准库把 Error::source() 定义为跨抽象层暴露底层错误原因的机制:上层模块可以返回自己的错误,同时保留导致它发生的低层错误,便于诊断。(Rust 文档)
三、为什么需要错误链?
假设业务执行流程是:
创建用户 ↓ 执行数据库 SQL ↓ 连接 MySQL
最终 MySQL 连接失败。
如果只保存字符串:
AppError::Internal("创建用户失败".to_string())
调用者只能看到:
创建用户失败
无法知道具体原因。
使用 #[source] 后,可以构成错误链:
创建用户失败 ↓ 数据库操作失败 ↓ 数据库连接超时
这样日志系统或错误报告工具就可以逐层打印错误原因。
四、多层错误链示例
先定义最底层错误:
use thiserror::Error;
#[derive(Debug, Error)]
pub enum NetworkError {
#[error("连接服务器超时")]
Timeout,
#[error("DNS 解析失败")]
DnsFailed,
}
数据库错误包含网络错误:
#[derive(Debug, Error)]
pub enum DbErr {
#[error("无法连接数据库")]
Connection {
#[source]
source: NetworkError,
},
#[error("SQL 执行失败:{0}")]
Sql(String),
}
应用错误又包含数据库错误:
#[derive(Debug, Error)]
pub enum AppError {
#[error("创建用户失败")]
CreateUser {
#[source]
source: DbErr,
},
}
构造完整错误:
let error = AppError::CreateUser {
source: DbErr::Connection {
source: NetworkError::Timeout,
},
};
错误链是:
AppError::CreateUser
↓
DbErr::Connection
↓
NetworkError::Timeout
打印错误链:
use std::error::Error;
fn print_error_chain(error: &(dyn Error + 'static)) {
println!("错误:{error}");
let mut current = error.source();
while let Some(source) = current {
println!("原因:{source}");
current = source.source();
}
}
fn main() {
let error = AppError::CreateUser {
source: DbErr::Connection {
source: NetworkError::Timeout,
},
};
print_error_chain(&error);
}
输出:
错误:创建用户失败 原因:无法连接数据库 原因:连接服务器超时
这就是 #[source] 最重要的价值:保留并暴露完整的错误原因链。
五、字段名叫 source 时可以省略属性
下面的写法:
#[derive(Debug, Error)]
pub enum AppError {
#[error("数据库操作失败")]
Database {
#[source]
error: DbErr,
},
}
因为字段名是 error,所以需要明确标记:
#[source]
但是字段名直接叫 source 时:
#[derive(Debug, Error)]
pub enum AppError {
#[error("数据库操作失败")]
Database {
source: DbErr,
},
}
可以省略:
#[source]
thiserror 会自动把名为 source 的字段识别为底层错误来源。(Docs.rs)
不过为了代码含义更明显,也可以继续明确写出来:
#[derive(Debug, Error)]
pub enum AppError {
#[error("数据库操作失败")]
Database {
#[source]
source: DbErr,
},
}
六、#[source] 不会自动实现错误转换
这是它和 #[from] 最关键的区别。
下面只有 #[source]:
#[derive(Debug, Error)]
pub enum AppError {
#[error("数据库操作失败")]
Database {
#[source]
source: DbErr,
},
}
它只表示:
DbErr 是 AppError 的底层原因
但它不会自动生成:
impl From<DbErr> for AppError
因此,下面的代码通常不能直接使用 ?:
async fn find_user() -> Result<(), AppError> {
database_operation().await?;
Ok(())
}
因为:
database_operation().await
返回的是:
Result<(), DbErr>
当前函数返回的却是:
Result<(), AppError>
而你没有定义:
DbErr → AppError
的转换规则。
使用 #[source] 时手动转换
需要使用 map_err():
async fn find_user() -> Result<(), AppError> {
database_operation()
.await
.map_err(|source| AppError::Database { source })?;
Ok(())
}
也可以拆开写:
async fn find_user() -> Result<(), AppError> {
let result = database_operation().await;
match result {
Ok(()) => Ok(()),
Err(source) => {
Err(AppError::Database { source })
}
}
}
七、#[from] 会自动实现转换
改成:
#[derive(Debug, Error)]
pub enum AppError {
#[error("数据库操作失败:{0}")]
Database(#[from] DbErr),
}
thiserror 会自动生成类似代码:
impl From<DbErr> for AppError {
fn from(value: DbErr) -> Self {
AppError::Database(value)
}
}
因此可以直接使用:
async fn find_user() -> Result<(), AppError> {
database_operation().await?;
Ok(())
}
当数据库方法返回:
Err(DbErr::ConnectionTimeout)
? 会自动完成:
DbErr::ConnectionTimeout
↓
AppError::Database(DbErr::ConnectionTimeout)
八、#[from] 自动包含 #[source]
下面的写法:
Database(#[from] DbErr)
不仅会自动生成:
impl From<DbErr> for AppError
还会自动把 DbErr 当作底层错误来源。
也就是说:
#[from]
本身就隐含:
#[source]
因此不需要同时写:
Database(#[from] #[source] DbErr)
直接写:
Database(#[from] DbErr)
即可。thiserror 官方文档明确说明:#[from] 总是意味着同一字段也是 #[source]。(Docs.rs)
九、#[source] 和 #[from] 的区别
| 对比项 | #[source] |
#[from] |
|---|---|---|
| 标记底层错误原因 | 是 | 是 |
实现 Error::source() |
是 | 是 |
| 建立错误链 | 是 | 是 |
自动实现 From<T> |
否 | 是 |
支持通过 ? 自动转换 |
否 | 是 |
| 可以附带额外业务字段 | 可以 | 通常不可以 |
| 主要用途 | 保留错误原因和额外上下文 | 简单、自动地转换错误 |
可以简单记成:
#[source] = 这是谁导致的 #[from] = 如何从底层错误自动转换过来
或者:
#[source]:只建立错误链 #[from]:错误链 + 自动类型转换
十、什么时候使用 #[source]?
当你需要在错误中添加额外上下文时,通常使用 #[source]。
例如,除了数据库错误,还想记录:
- 哪个用户操作失败;
- 执行了什么操作;
- 哪张数据表出错;
- 哪个第三方接口出错;
- 请求 ID 是多少。
#[derive(Debug, Error)]
pub enum AppError {
#[error("保存用户失败,用户 ID:{user_id}")]
SaveUser {
user_id: i64,
#[source]
source: DbErr,
},
}
构造错误:
async fn save_user(user_id: i64) -> Result<(), AppError> {
database_operation()
.await
.map_err(|source| AppError::SaveUser {
user_id,
source,
})?;
Ok(())
}
发生错误后,上层信息是:
保存用户失败,用户 ID:1001
底层原因是:
数据库连接超时
这样既有业务上下文,也保留了原始数据库错误。
十一、为什么这种情况不使用 #[from]?
你可能会尝试:
#[derive(Debug, Error)]
pub enum AppError {
#[error("保存用户失败,用户 ID:{user_id}")]
SaveUser {
user_id: i64,
#[from]
source: DbErr,
},
}
这种设计存在问题:只根据一个 DbErr,无法自动知道 user_id 应该是多少。
假设生成:
impl From<DbErr> for AppError {
fn from(source: DbErr) -> Self {
AppError::SaveUser {
user_id: ???,
source,
}
}
}
转换过程中只有 DbErr,并没有 user_id,因此无法自动构造完整的 AppError::SaveUser。
thiserror 对 #[from] 变体有相应限制:除了错误来源字段以及特定的回溯字段,通常不能再带普通附加字段。(Docs.rs)
这种情况下应该使用:
#[source]
然后手动通过 map_err() 补充上下文:
.map_err(|source| AppError::SaveUser {
user_id,
source,
})?
十二、#[from] 适合什么场景?
当错误转换不需要额外上下文时,用 #[from] 最方便。
#[derive(Debug, Error)]
pub enum AppError {
#[error("数据库错误:{0}")]
Database(#[from] DbErr),
#[error("第三方接口错误:{0}")]
Api(#[from] ApiError),
}
业务代码可以直接写:
async fn process() -> Result<(), AppError> {
database_operation().await?;
call_api().await?;
Ok(())
}
转换关系:
DbErr ↓ #[from] AppError::Database(DbErr)
以及:
ApiError ↓ #[from] AppError::Api(ApiError)
十三、#[source] 适合什么场景?
需要补充业务上下文时,使用 #[source]:
#[derive(Debug, Error)]
pub enum AppError {
#[error("保存用户失败,用户 ID:{user_id}")]
SaveUser {
user_id: i64,
#[source]
source: DbErr,
},
#[error("调用第三方接口失败,接口名称:{api_name}")]
CallApi {
api_name: String,
#[source]
source: ApiError,
},
}
处理数据库错误:
async fn save_user(user_id: i64) -> Result<(), AppError> {
database_operation()
.await
.map_err(|source| AppError::SaveUser {
user_id,
source,
})?;
Ok(())
}
处理第三方接口错误:
async fn send_message() -> Result<(), AppError> {
call_api()
.await
.map_err(|source| AppError::CallApi {
api_name: "短信服务".to_string(),
source,
})?;
Ok(())
}
十四、实际项目推荐设计
在你的 Rust Web 项目中,可以将简单底层错误使用 #[from]:
#[derive(Debug, thiserror::Error)]
pub enum AppError {
#[error("数据库错误:{0}")]
Database(#[from] sea_orm::DbErr),
#[error("JSON 处理错误:{0}")]
Json(#[from] serde_json::Error),
#[error("IO 错误:{0}")]
Io(#[from] std::io::Error),
}
这些错误不需要添加额外信息,因此可以直接:
let user = user::Entity::find_by_id(user_id)
.one(db)
.await?;
let content = std::fs::read_to_string(path)?;
let value = serde_json::from_str(&content)?;
而需要业务上下文的错误使用 #[source]:
#[derive(Debug, thiserror::Error)]
pub enum AppError {
#[error("查询用户失败,用户 ID:{user_id}")]
QueryUser {
user_id: i64,
#[source]
source: sea_orm::DbErr,
},
#[error("读取配置文件失败,文件路径:{path}")]
ReadConfig {
path: String,
#[source]
source: std::io::Error,
},
}
使用:
pub async fn find_user(
db: &sea_orm::DatabaseConnection,
user_id: i64,
) -> Result<Option<user::Model>, AppError> {
user::Entity::find_by_id(user_id)
.one(db)
.await
.map_err(|source| AppError::QueryUser {
user_id,
source,
})
}
这种错误信息比单纯的:
数据库错误:连接超时
更有排查价值:
查询用户失败,用户 ID:1001 原因:数据库连接超时
十五、一句话总结
#[source]
表示:
这个字段保存了导致当前错误发生的底层错误,用来建立错误原因链。
#[from]
表示:
自动实现从底层错误到当前错误的
From转换,并同时把底层错误标记为source。
因此:
#[source] = 错误原因链 #[from] = 自动转换 + 错误原因链
选择原则是:
只需要简单转换,并希望直接使用 ?: 使用 #[from] 需要加入 user_id、文件路径、接口名称等额外上下文: 使用 #[source] + map_err()