常用生态库 intermediate 30 分钟 更新 2026-06-15

anyhow 与 thiserror

用 thiserror 定义库错误,用 anyhow 聚合应用错误。

anyhow 与 thiserror

你会学到什么

  • thiserror 用派生宏自动实现 DisplayError,省去样板代码。
  • anyhow 提供一个“万能错误类型”,适合应用层快速聚合。
  • 经验法则:库用 thiserror(精确),应用用 anyhow(方便)。

最小示例

use thiserror::Error;

#[derive(Error, Debug)]
enum ConfigError {
    #[error("配置为空")]
    Empty,
    #[error("端口不是数字: {0}")]
    NotANumber(String),
}

运行代码

cd examples
cargo run -p rt_35_error_libs
cargo test -p rt_35_error_libs

依赖:

[dependencies]
anyhow = "1"
thiserror = "2"

代码讲解

回忆“自定义错误类型”那一章,我们手写了 DisplayErrorthiserror#[error("...")] 把这些一行搞定,{0} 引用元组字段:

#[error("端口超出范围: {0}")]
OutOfRange(u32),

应用层函数返回 anyhow::Result<T>,? 能自动把任何实现了 Error 的类型转换进去:

fn load_config(input: &str) -> anyhow::Result<u16> {
    let port = parse_port(input)?; // ConfigError 自动转成 anyhow::Error
    Ok(port)
}

anyhow 还支持 .context("加载配置失败") 给错误加上下文,排查问题时非常有用。

常见错误

在库的公开 API 里直接用 anyhow::Error 当返回类型,会让调用方无法按变体匹配错误。库应当暴露具体的 thiserror 类型。

练习

  • ConfigError 加一个 #[error("缺少字段: {0}")] 变体。
  • load_config 里用 .context(...) 给错误补充上下文。

小结

thiserror 让定义精确错误几乎零样板,anyhow 让应用层聚合错误轻松自如,二者常配合使用。

下一步

接着学习用 clap 解析命令行参数。

完整示例代码

下面是 examples/35_error_libs/ 的完整源码。无需 clone 仓库,直接在页面上阅读、复制、对照运行。

examples/35_error_libs/src/main.rs
//! thiserror 定义错误类型,anyhow 在应用层聚合错误。

use thiserror::Error;

/// thiserror 自动生成 Display 和 Error 实现。
#[derive(Error, Debug, PartialEq)]
enum ConfigError {
    #[error("配置为空")]
    Empty,
    #[error("端口不是数字: {0}")]
    NotANumber(String),
    #[error("端口超出范围: {0}")]
    OutOfRange(u32),
}

/// 库层:返回精确的自定义错误。
fn parse_port(input: &str) -> Result<u16, ConfigError> {
    let trimmed = input.trim();
    if trimmed.is_empty() {
        return Err(ConfigError::Empty);
    }
    let value: u32 = trimmed
        .parse()
        .map_err(|_| ConfigError::NotANumber(trimmed.to_string()))?;
    if value > u16::MAX as u32 {
        return Err(ConfigError::OutOfRange(value));
    }
    Ok(value as u16)
}

/// 应用层:用 anyhow::Result 聚合不同来源的错误,`?` 自动转换。
fn load_config(input: &str) -> anyhow::Result<u16> {
    let port = parse_port(input)?;
    Ok(port)
}

fn main() -> anyhow::Result<()> {
    let port = load_config("8080")?;
    println!("port = {port}");
    println!("error demo: {}", parse_port("abc").unwrap_err());
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parses_valid_port() {
        assert_eq!(parse_port("8080"), Ok(8080));
    }

    #[test]
    fn reports_specific_errors() {
        assert_eq!(parse_port(""), Err(ConfigError::Empty));
        assert_eq!(
            parse_port("abc"),
            Err(ConfigError::NotANumber("abc".to_string()))
        );
        assert_eq!(parse_port("70000"), Err(ConfigError::OutOfRange(70000)));
    }

    #[test]
    fn anyhow_wraps_error() {
        assert!(load_config("abc").is_err());
        assert_eq!(load_config("443").unwrap(), 443);
    }
}
examples/35_error_libs/Cargo.toml
[package]
name = "rt_35_error_libs"
version.workspace = true
edition.workspace = true
publish.workspace = true

[dependencies]
anyhow = "1"
thiserror = "2"