Rust:错误处理的anyhow与thiserror

Rust的错误处理是让新手最头疼的部分之一。标准库的ResultError trait提供了基础,但实际项目中直接用它们会很啰嗦。thiserroranyhow这两个crate分别解决了库代码和应用代码的错误处理痛点。

标准库的Error trait

先看标准库提供了什么:

// std::error::Error trait
pub trait Error: Debug + Display {
    fn source(&self) -> Option<&(dyn Error + 'static)> { None }
}

要实现一个自定义错误类型,需要:

use std::fmt;

#[derive(Debug)]
enum AppError {
    Database(String),
    NotFound(u64),
    InvalidInput { field: String, reason: String },
}

impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            AppError::Database(msg) => write!(f, "database error: {}", msg),
            AppError::NotFound(id) => write!(f, "resource {} not found", id),
            AppError::InvalidInput { field, reason } =>
                write!(f, "invalid input for '{}': {}", field, reason),
        }
    }
}

impl std::error::Error for AppError {}

// 如果要从其他错误类型转换,还需要impl From
impl From<sqlx::Error> for AppError {
    fn from(e: sqlx::Error) -> Self {
        AppError::Database(e.to_string())
    }
}

一个错误类型就要写这么多样板代码。如果有十种错误变体,每个都有不同的source,代码量会膨胀到让人崩溃。

thiserror:给库代码用

thiserror是一个derive宏,自动生成DisplayError的实现:

use thiserror::Error;

#[derive(Debug, Error)]
enum AppError {
    #[error("database error: {0}")]
    Database(#[from] sqlx::Error),

    #[error("resource {0} not found")]
    NotFound(u64),

    #[error("invalid input for '{field}': {reason}")]
    InvalidInput { field: String, reason: String },

    #[error("authentication failed")]
    Unauthorized,

    #[error(transparent)]
    Other(#[from] anyhow::Error),
}

几个关键特性:

  • #[error("...")]:自动生成Display实现,支持格式化语法
  • #[from]:自动生成From<T>实现,配合?操作符实现自动转换
  • #[source]:标记error chain中的上游错误
  • #[error(transparent)]:直接透传inner error的Display和source

上面的代码展开后等价于手写的几十行样板代码。

什么时候用thiserror:当你在写库(library)或模块,需要定义明确的、结构化的错误类型,让调用者能match具体的错误变体时。

anyhow:给应用代码用

anyhow走的是另一个方向——不定义具体的错误类型,用一个通用的anyhow::Error承接所有错误:

use anyhow::{Context, Result, bail, ensure};

// Result<T> 是 Result<T, anyhow::Error> 的别名
fn load_config(path: &str) -> Result<Config> {
    let content = std::fs::read_to_string(path)
        .context(format!("failed to read config file: {}", path))?;

    let config: Config = serde_json::from_str(&content)
        .context("failed to parse config as JSON")?;

    ensure!(config.port > 0, "port must be positive, got {}", config.port);

    if config.database_url.is_empty() {
        bail!("database_url cannot be empty");
    }

    Ok(config)
}

核心API:

  • anyhow::Result<T>Result<T, anyhow::Error>的别名
  • .context("msg"):给错误添加上下文信息,形成error chain
  • bail!("msg"):类似return Err(anyhow!("msg"))
  • ensure!(condition, "msg"):类似assert但返回Error而不是panic
  • anyhow!("msg"):创建一个anyhow::Error

?操作符与错误转换

?操作符是Rust错误处理的核心语法糖:

fn process() -> Result<Data> {
    let file = File::open("data.txt")?;   // io::Error → anyhow::Error
    let content = read_all(file)?;          // io::Error → anyhow::Error
    let data = parse(content)?;             // ParseError → anyhow::Error
    Ok(data)
}

?的行为:如果Result是Ok,解包值继续执行;如果是Err,调用From::from()转换错误类型并提前返回。

对于anyhow,几乎所有实现了std::error::Error的类型都能自动转换为anyhow::Error

对于thiserror,只有标记了#[from]的变体才能自动转换,其他的需要手动.map_err()

错误链(Error Chain)

anyhow的.context()会构建一个错误链,打印时层层展示:

fn read_user(id: u64) -> Result<User> {
    let row = db.query("SELECT * FROM users WHERE id = ?", id)
        .context("failed to query database")?;
    let user = parse_row(row)
        .context(format!("failed to parse user {}", id))?;
    Ok(user)
}

// 错误输出:
// Error: failed to parse user 42
//
// Caused by:
//    0: failed to query database
//    1: connection refused (os error 111)

这比直接看到"connection refused"有用太多了。

thiserror vs anyhow:怎么选

一句话总结:库用thiserror,应用用anyhow

thiserror anyhow
适用场景 库/模块 应用程序
错误类型 自定义enum 通用anyhow::Error
调用者能否match 不能(除非downcast)
样板代码 少(derive宏) 几乎没有
错误上下文 手动实现source .context()自动链式

如果你写的是一个供别人调用的库,调用者需要根据不同错误类型做不同处理(比如数据库错误重试、权限错误返回403),用thiserror定义清晰的错误enum。

如果你写的是最终的应用程序(Web服务器、CLI工具),大部分错误最终都是打日志或返回500,用anyhow省心省力。

实际项目中两者经常混用:底层模块用thiserror定义结构化错误,上层应用用anyhow统一处理。thiserror定义的错误类型实现了std::error::Error,所以能被?自动转换为anyhow::Error

// 底层模块
mod storage {
    use thiserror::Error;

    #[derive(Debug, Error)]
    pub enum StorageError {
        #[error("key not found: {0}")]
        NotFound(String),
        #[error("io error")]
        Io(#[from] std::io::Error),
    }

    pub fn get(key: &str) -> Result<Vec<u8>, StorageError> {
        // ...
    }
}

// 上层应用
use anyhow::{Context, Result};

fn handle_request(key: &str) -> Result<String> {
    let data = storage::get(key)
        .context("storage lookup failed")?;  // StorageError → anyhow::Error
    let text = String::from_utf8(data)
        .context("invalid utf-8 in stored data")?;
    Ok(text)
}

这是Rust社区广泛采用的错误处理模式,简洁又不失灵活。