bigywater.tech
Rust · 74

thiserror 与 anyhow 的边界

thiserror 与 anyhow 常被放在一起比较,其实它们解决的是两件事。这篇真的下载编译这两个 crate,展开 thiserror 的宏看它生成了什么,再用 anyhow 的链式上下文,最后用一个例子说明两者的边界。

8 字节
anyhow::Error 的大小
63 行
thiserror 为一个 3 变体枚举生成的 impl 代码行数(含 Debug)
2
这两个 crate 各解决的问题:定义错误类型 / 报告并叠加上下文

thiserror 的 derive

实测 · 本机执行输出
  运行结果(退出码 0):
    12       -> Ok(12)
    missing  -> Display: 键 missing 不存在   source: None   Debug: NotFound("missing")
    io       -> Display: 读取 /不存在 失败   source: Some("entity not found")   Debug: Io { path: "/不存在", source: Kind(NotFound) }
    abc      -> Display: 无效数字   source: Some("invalid digit found in string")   Debug: Parse(ParseIntError { kind: InvalidDigit })
    size_of::<StoreError>() = 32

手写版本见自定义错误类型。thiserror 从 crates.io 下载编译(2.0.21),需要联网。

宏展开后的代码

实测 · 本机执行输出
  展开后与 StoreError 相关的 impl(共 63 行,下面显示前 70 行):
    impl ::core::fmt::Debug for StoreError {
        #[inline]
        fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
            match self {
                StoreError::NotFound(__self_0) =>
                    ::core::fmt::Formatter::debug_tuple_field1_finish(f,
                        "NotFound", &__self_0),
                StoreError::Io { path: __self_0, source: __self_1 } =>
                    ::core::fmt::Formatter::debug_struct_field2_finish(f, "Io",
                        "path", __self_0, "source", &__self_1),
                StoreError::Parse(__self_0) =>
                    ::core::fmt::Formatter::debug_tuple_field1_finish(f, "Parse",
                        &__self_0),
            }
        }
    }
    impl ::thiserror::__private21::Error for StoreError {
        fn source(&self)
            ->
                ::core::option::Option<&(dyn ::thiserror::__private21::Error +
                'static)> {
            use ::thiserror::__private21::AsDynError as _;
    
            #[allow(deprecated)]
            match self {
                StoreError::NotFound { .. } => ::core::option::Option::None,
                StoreError::Io { source: source, .. } =>
                    ::core::option::Option::Some(source.as_dyn_error()),
                StoreError::Parse { 0: source, .. } =>
                    ::core::option::Option::Some(source.as_dyn_error()),
            }
        }
    }
    impl ::core::fmt::Display for StoreError {
        fn fmt(&self, __formatter: &mut ::core::fmt::Formatter)
            -> ::core::fmt::Result {
            use ::thiserror::__private21::AsDisplay as _;
    
            #[allow(unused_variables, deprecated, clippy ::
            used_underscore_binding)]
            match self {
                StoreError::NotFound(_0) =>
                    match (_0.as_display(),) {
                        (__display0,) =>
                            __formatter.write_fmt(format_args!("键 {0} 不存在",
                                    __display0)),
                    },
                StoreError::Io { path, source } =>
                    match (path.as_display(),) {
                        (__display_path,) =>
                            __formatter.write_fmt(format_args!("读取 {0} 失败",
                                    __display_path)),
                    },
                StoreError::Parse(_0) => __formatter.write_str("无效数字"),
            }
        }
    }
    impl ::core::convert::From<std::num::ParseIntError> for StoreError {
        #[allow(clippy :: redundant_field_names)]
        fn from(source: std::num::ParseIntError) -> Self {
            StoreError::Parse { 0: source }
        }
    }

anyhow 的上下文链、Debug 与体积

实测 · 本机执行输出
  运行结果(退出码 0):
    监听 8080
    --- 输入 ""
    {}   : 启动服务失败
    {:#}  : 启动服务失败: 端口为空
    {:?}  :
    启动服务失败
    
    Caused by:
        端口为空
    原因链: ["启动服务失败", "端口为空"]
    --- 输入 "80"
    {}   : 启动服务失败
    {:#}  : 启动服务失败: 端口 80 是保留端口
    {:?}  :
    启动服务失败
    
    Caused by:
        端口 80 是保留端口
    原因链: ["启动服务失败", "端口 80 是保留端口"]
    --- 输入 "abc"
    {}   : 启动服务失败
    {:#}  : 启动服务失败: 解析端口 "abc" 失败: invalid digit found in string
    {:?}  :
    启动服务失败
    
    Caused by:
        0: 解析端口 "abc" 失败
        1: invalid digit found in string
    原因链: ["启动服务失败", "解析端口 \"abc\" 失败", "invalid digit found in string"]
    size_of::<anyhow::Error>() = 8  size_of::<Result<(), anyhow::Error>>() = 8
    downcast_ref::<ParseIntError>: Some("invalid digit found in string")

库与应用的边界

实测 · 本机执行输出
  运行结果(退出码 0):
    typed:调用方 match 到 Timeout,可以按 5 秒重试
    anyhow:调用方拿到的是 超时 5 秒
    只能 downcast_ref 才能还原:Timeout(5)

? 的展开见? 运算符展开过程。

分步演示

thiserror 与 anyhow
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / rustc 1.98.1(brew)/ cargo 1.98.1;单个文件用 rustc 直接编译,需要依赖时用 cargo(经代理从 crates.io 下载);计时用 std::time::Instant,数字每次运行会有小幅抖动。crates:thiserror 2.0.21、anyhow 1.0.104(通过代理从 crates.io 下载)
    • 没有验证:anyhow 的 backtrace(需要环境变量与 std 支持)、编译时间对比、no_std 场景、thiserror 的 #[error(transparent)] 与 #[backtrace]、eyre / snafu 等替代品
    京ICP备2026067502号-1京公网安备11010502063487号