外观
雷区 7:anyhow 与 thiserror,谁用哪个?
症状
新手(包括很多老手)面对"错误处理"时常见的两个困惑:
- 手写错误处理好麻烦:
map_err+format!+ 自定义枚举 + 手写Display……一个库还没写,错误代码先写 50 行 ?为什么这么难用:这个函数返回io::Error,那个返回ParseIntError,?直接报"错误类型不匹配"
其实 Rust 生态早有两个标准答案:anyhow 和 thiserror——它们是同一枚硬币的两面,各管一个场景。
诊断:两个工具的分工
anyhow | thiserror | |
|---|---|---|
| 给谁用 | 应用(binary) | 库(library) |
| 干什么 | 错误随便往 ? 里扔,自动带上上下文 | 用 #[derive] 自动生成自定义错误 |
| 错误类型 | anyhow::Error(万能收纳盒) | 你自己的 XxxError 枚举 |
| 调用方 | 只需要打印/报告 | 需要 match 按类型处理 |
| 一句话 | "别管错误细节,抛出去就行" | "把错误类型设计得漂漂亮亮" |
判断口诀:应用用 anyhow,库用 thiserror。
为什么?库的用户需要区分错误类型来决定行为(重试?换参数?提示?),所以库必须暴露结构化的自定义错误 → thiserror 帮你少写样板;应用的错误最终只是"报告给用户/日志",没人会 match 你的应用错误 → anyhow 帮你少写类型。
解药一:应用里用 anyhow
toml
[dependencies]
anyhow = "1"rust
use anyhow::{Context, Result};
fn load_user() -> Result<String> {
let content =
std::fs::read_to_string("user.txt").context("读取用户文件失败")?;
Ok(content)
}
fn main() -> Result<()> {
let user = load_user()?;
println!("用户:{}", user);
Ok(())
}.context("……") 是 anyhow 的灵魂:给错误裹上"人在哪一步"的信息,错误链层层叠起来。出错时输出:
运行输出
text
Error: 读取用户文件失败
Caused by:
系统找不到指定的文件。(os error 2)main -> Result<()> 直接可用(雷区 2 的解药!),任何错误类型都能 ? 进 anyhow::Error——io::Error、ParseIntError、自定义枚举,统统收。
解药二:库的公共 API 用 thiserror
toml
[dependencies]
thiserror = "2"rust
use thiserror::Error;
#[derive(Error, Debug)]
enum AppError {
#[error("找不到文件:{0}")]
FileNotFound(String),
#[error("第 {line} 行解析失败:{reason}")]
ParseFailed { line: u32, reason: String },
}
fn load_config() -> Result<(), AppError> {
let content = std::fs::read_to_string("config.txt")
.map_err(|_| AppError::FileNotFound("config.txt".to_string()))?;
// ……
Ok(())
}#[error("……")] 一行,自动生成 Display(雷区 5 手写了 15 行)——{0} 是第一个字段,{line}/{reason} 是命名字段。错误枚举还可以自动转换标准错误:
rust
#[derive(Error, Debug)]
enum AppError {
#[error("配置文件读取失败")]
Io(#[from] std::io::Error), // io::Error 自动变 AppError,连 map_err 都不用写
#[error("第 {line} 行解析失败")]
ParseFailed { line: u32 },
}
fn load_config() -> Result<(), AppError> {
let content = std::fs::read_to_string("config.txt")?; // io::Error 自动转 AppError
Ok(())
}#[from] 是"自动转换":io::Error 能自动变成 AppError::Io——? 直接扔,连 .map_err 都省了。
预防
- 选择顺序:写库 → thiserror(自定义错误 +
#[from]);写应用 → anyhow(?+.context) - 两者可以共存:库用 thiserror 定义错误,应用用 anyhow 包裹——
anyhow::Error能收下任何 thiserror 错误,两个工具天然互补 - anyhow 是"最后报告",thiserror 是"类型设计":想清楚错误是"给人看的"还是"给代码处理的",答案就出来了
- 别在库的公共 API 里用 anyhow:用户没法
match你的anyhow::Error——这是库设计的大忌(此雷区标题就是纪律)