Rust的错误处理是让新手最头疼的部分之一。标准库的Result和Error trait提供了基础,但实际项目中直接用它们会很啰嗦。thiserror和anyhow这两个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宏,自动生成Display和Error的实现:
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 chainbail!("msg"):类似return Err(anyhow!("msg"))ensure!(condition, "msg"):类似assert但返回Error而不是panicanyhow!("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社区广泛采用的错误处理模式,简洁又不失灵活。