本页目录

Error trait 与生态

std::error::Error 只是 Display+Debug+source() 的组合——太薄了。thiserror 用 derive 宏给库生成结构化错误类型(调用方能 match),anyhow 给应用提供"不在意错误类型,只管向上传"的万能错误包装。库用 thiserror,应用用 anyhow——这不是教条,是实践中沉淀下来的分工。

std::error::Error trait

标准库的 Error trait 很简单——Display + Debug + source()

use std::error::Error;
use std::fmt;

#[derive(Debug)]
struct MyError { msg: String, source: Option<Box<dyn Error + 'static>> }

impl fmt::Display for MyError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    write!(f, "{}", self.msg)
}}

impl Error for MyError {
    fn source(&self) -> Option<&(dyn Error + 'static)> { self.source.as_deref() }
}

source() 方法返回底层原因——形成错误链。anyhoweyre 利用这个链自动生成 backtrace。

thiserror: library crate 的 derive macro

用于⁠——给 caller 提供可匹配的具体错误类型:

use thiserror::Error;

#[derive(Error, Debug)]
pub enum DataStoreError {
    #[error("data not found: {0}")]
    NotFound(String),
    #[error("IO error")]
    Io(#[from] std::io::Error),      // #[from] = 自动 impl From + 自动 source
    #[error("invalid config key={key}")]
    InvalidConfig { key: String, detail: String },
}

#[from] 自动生成 From<io::Error> for DataStoreError,使 ? 可以自动转换。caller 可以用 match 区分错误类型。

anyhow: application code 的通用错误容器

用于应用/二进制⁠——收集错误链,不关心具体类型:

use anyhow::{Context, Result};

fn read_config(path: &str) -> Result<Config> {
    let file = std::fs::read_to_string(path)
        .with_context(|| format!("failed to read config from {}", path))?;
    let config: Config = toml::from_str(&file)
        .context("invalid TOML format")?;
    Ok(config)
}

anyhow::Error 可以存储任何 Error + Send + Sync + 'staticcontext() 给错误链添加人类可读的注释——这是 anyhow 最常用的模式。

何时用 thiserror vs anyhow

一条简单规则:⁠库用 thiserror(给 caller choice),应用用 anyhow(收集错误上下文)。库不应该强制 caller 使用特定的错误报告库——抛具体的、可匹配的类型。应用不需要区分错误类型——需要的是所有错误都被捕获、记录,并有足够的上下文重现 bug。

参考

  • thiserror: docs.rs/thiserror
  • anyhow: docs.rs/anyhow

Keywords: Error trait, thiserror, anyhow, context, source, error chain, #[from]