不灭的焱

革命尚未成功,同志仍须努力 下载Java21

作者:AlbertWen  添加时间:2026-07-22 23:53:44  修改时间:2026-07-26 23:26:12  分类:01.Rust编程  编辑

目录

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())

它做了两件事:

  1. 使用 value.to_string()DbErr 转换成字符串;
  2. 将错误字符串包装成 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)
}

不管底层产生:

  • DbErr
  • ApiClientError
  • 参数校验错误
  • 权限错误

最终都会统一为:

AppError

十三、总结

你的代码:

impl From<DbErr> for AppError {
    fn from(value: DbErr) -> Self {
        Self::Internal(value.to_string())
    }
}

主要有三个作用:

  1. 建立 DbErr → AppError 的转换规则;
  2. 允许使用 db_err.into() 转换错误;
  3. 允许在返回 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),
}

这样它会自动实现:

  • Debug
  • Display
  • std::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()