错误处理工程实践
thiserror与anyhow的分工、错误处理的项目级决策。
工程实践
库 API 的错误类型应当是 enum 且 #[non_exhaustive]
rust
use std::error::Error;
use std::fmt;
/// 解析配置时可能出现的全部错误。
/// #[non_exhaustive] 允许未来新增变体而不算破坏性变更。
#[non_exhaustive]
#[derive(Debug)]
pub enum ParseError {
MissingField(&'static str),
InvalidNumber { field: &'static str, value: String },
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ParseError::MissingField(name) => write!(f, "缺少字段 `{name}`"),
ParseError::InvalidNumber { field, value } => {
write!(f, "字段 `{field}` 的值 `{value}` 不是数字")
}
}
}
}
impl Error for ParseError {}
fn main() {
let e = ParseError::InvalidNumber { field: "port", value: "abc".into() };
println!("{e}"); // Display 只打印业务含义,不打印类型名
}输出:
text字段 `port` 的值 `abc` 不是数字
为什么加 #[non_exhaustive]?因为它强制下游 crate 写兜底分支。把下面这段想象成另一个 crate 里的代码(ParseError 从你的库导入):
rust
use std::error::Error;
use std::fmt;
// ===== 这一段属于你的库 crate =====
#[non_exhaustive]
#[derive(Debug)]
pub enum ParseError {
MissingField(&'static str),
InvalidNumber { field: &'static str, value: String },
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ParseError::MissingField(name) => write!(f, "缺少字段 `{name}`"),
ParseError::InvalidNumber { field, value } => {
write!(f, "字段 `{field}` 的值 `{value}` 不是数字")
}
}
}
}
impl Error for ParseError {}
// ===== 这一段是下游 crate 的代码:必须带 `_ =>` =====
// 若删掉 `_ =>` 分支,会报 E0004: non-exhaustive patterns:
// `&ParseError` not covered
// 因为对下游而言,这个 enum「可能还有别的变体」。
#[allow(unreachable_patterns)] // 本文件里 enum 定义在同 crate,否则 Rust 会提醒兜底分支当前不可达
fn describe(e: ParseError) -> String {
match e {
ParseError::MissingField(name) => format!("缺少 {name}"),
ParseError::InvalidNumber { field, .. } => format!("{field} 不是数字"),
_ => "未知的解析错误".to_string(), // 下游侧:这一行是**必需**的
}
}
fn main() {
println!("{}", describe(ParseError::MissingField("port")));
}输出:
text缺少 port
🧠 原理:
#[non_exhaustive]让enum在跨 crate 使用时被视为「可能还有其他变体」,只有在定义它的 crate 内部才允许省略兜底分支。于是你发布1.1.0并新增一个变体后,下游不会因为match不完整而编译失败——这是「错误enum是 API 表面」的正确态度。副作用要在脑子里记住:同一个
match,在库内部写是「穷尽匹配」(编译器帮你查漏),在下游写就变成「必须带_」(编译器不再帮你查漏)。上面代码里的#[allow(unreachable_patterns)]正是为了让两种视角在同一个文件里都能编译;真实项目里库内和下游分属两个 crate,这个属性就不需要了。⚠️ 陷阱:
#[non_exhaustive]也是双刃剑:下游从此无法对自己的match做穷尽性检查,新增变体会静默落进_分支。所以它适合「错误种类预期还会增长」的库;如果错误集合是稳定的,不加反而更好。
💡 对照:Java 里给异常类加子类是兼容的(
catch只匹配已知类型),Rust 的enum+match默认要求穷尽,所以才需要#[non_exhaustive]这个显式开关。
binary crate 的 main 返回什么
| 需求 | main 写法 |
|---|---|
| 简单工具,失败就 panic(可接受) | fn main() + unwrap() |
| 有错误要报告,退出码 1 即可 | fn main() -> Result<(), Box<dyn Error + Send + Sync>> |
想用 .context() 加人话 | fn main() -> anyhow::Result<()> |
| 需要区分退出码(0/1/2/64…) | fn main() -> std::process::ExitCode |
需要自定义错误打印格式(不带 Error: 前缀) | fn main() { if let Err(e) = run() { eprintln!("{e:?}"); std::process::exit(1); } } |
rust
use std::process::ExitCode;
fn run(args: &[String]) -> Result<i32, String> {
let first = args.first().ok_or("请传入一个整数参数")?;
let n: i32 = first.parse().map_err(|e| format!("`{first}` 不是整数: {e}"))?;
Ok(n * 2)
}
fn main() -> ExitCode {
let args: Vec<String> = std::env::args().skip(1).collect();
match run(&args) {
Ok(v) => {
println!("{v}");
ExitCode::SUCCESS
}
Err(msg) => {
eprintln!("用法错误:{msg}");
ExitCode::from(2) // 2 是 Unix 工具里「用法错误」的约定退出码
}
}
}unwrap() 的合法场景
unwrap() 会让进程/线程崩溃,所以它必须被论证为安全。下面这些场景是业界共识:
| 场景 | 写法 | 为什么可以 |
|---|---|---|
| 测试代码 | let v: i32 = "1".parse().unwrap(); | panic 就是测试失败,信息越直接越好 |
| 原型 / 一次性脚本 | fs::read_to_string("x").unwrap() | 先跑通逻辑,之后再改 ? |
| 常量字面量解析 | "8080".parse::<u16>().unwrap() | 字面量在编译期就能人眼验证,不可能失败 |
Mutex / RwLock 锁毒化 | lock().unwrap() | 毒化只可能来自持锁线程 panic;若无处 catch_unwind,进程早已在恢复状态不可信的情况下继续 |
| 已有前置检查保证 | if arr.len() > i { arr.get(i).unwrap() } | 不变量刚刚验证过;必须写注释说明依据 |
用 unwrap_or_else(|| unreachable!()) 替代 | x.ok_or(()).unwrap_or_else(|_| unreachable!()) | 明确「这里失败就是 bug」 |
rust
use std::sync::Mutex;
/// 计数器自增。`lock().unwrap()` 的理由:毒化只可能来自另一个线程 panic,
/// 而我们的临界区只有 `+= 1`,不存在「半更新状态」,因此可以安全忽略毒化。
fn inc(m: &Mutex<u32>) -> u32 {
let mut guard = m.lock().unwrap();
*guard += 1;
*guard
}
fn main() {
let m = Mutex::new(0);
println!("{} {}", inc(&m), inc(&m));
}输出:
text1 2
expect() 的消息怎么写
expect / unwrap 在 Result 与 Option 上的 panic 消息不一样,先记住原文(作者实测):
| 调用 | panic 消息 |
|---|---|
opt.unwrap()(None) | called `Option::unwrap()` on a `None` value |
opt.expect("配置文件必须包含端口") | 配置文件必须包含端口 |
res.unwrap()(Err) | called `Result::unwrap()` on an `Err` value: ParseIntError { kind: InvalidDigit } |
res.expect("端口必须是整数") | 端口必须是整数: ParseIntError { kind: InvalidDigit } |
结论:expect 的消息会替换掉「called X::unwrap() on …」那半句,但保留 Err 的 Debug 输出(Option 的 None 没有额外信息)。
expect 消息的写法规范:
- 写「期望什么」而不是「什么失败了」:
expect("依赖注入的 logger 必须已初始化")。 - 说明为什么不可能失败:
expect("字面量 \"8080\" 一定能解析成 u16")。 - 用肯定句:不要写
expect("没找到")(读到 panic 的人不知道在找什么)。 - 带上关键变量的值:
expect(&format!("配置 {key:?} 已在上一步校验过"))。 - 消息是给人看的,不用加「Error:」前缀——runtime 已经打印了
panicked at和位置。
用 #[cfg(test)] 断言 Err 变体
不要在测试里 assert!(result.is_err()) 就完事——那没验证是哪个错误。三种推荐写法:
rust
use std::collections::HashMap;
fn get_port(map: &HashMap<String, String>) -> Result<u16, String> {
let raw = map.get("port").ok_or("配置缺少 port 字段")?;
raw.parse::<u16>().map_err(|e| format!("port 不是合法端口号: {e}"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn missing_port_reports_field_name() {
let map = HashMap::new();
// unwrap_err 要求 Ok 侧实现 Debug;拿到 Err 再断言消息内容
let err = get_port(&map).unwrap_err();
assert!(err.contains("port"), "错误消息应包含字段名,实际是 {err}");
}
#[test]
fn out_of_range_port_is_rejected() {
let mut map = HashMap::new();
map.insert("port".into(), "99999".into()); // 超出 u16 范围
match get_port(&map) {
Err(msg) => assert!(msg.starts_with("port 不是合法端口号"), "{msg}"),
Ok(v) => panic!("99999 超出 u16 范围,不该成功:{v}"),
}
}
#[test]
fn empty_port_is_err() {
let mut map = HashMap::new();
map.insert("port".into(), String::new());
let err = get_port(&map).unwrap_err();
assert!(err.contains("port"), "{err}");
}
}powershell
cargo test对自定义 enum 错误,更进一步用模式匹配断言变体:
rust
// 这里用 E 代替真实项目里的错误类型,保持示例自包含
#[derive(Debug)]
enum E {
NotFound { path: String },
BadInt(String),
}
fn load(p: &str) -> Result<i32, E> {
Err(E::NotFound { path: p.into() })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn missing_file_maps_to_not_found_variant() {
match load("x.toml") {
// 断言「是哪一个变体」以及「变体里的字段值」
Err(E::NotFound { path }) => assert_eq!(path, "x.toml"),
other => panic!("期望 NotFound,实际 {other:?}"),
}
}
#[test]
fn matches_macro_for_variant_only() {
// 只关心变体、不关心字段时,matches! 最简洁
assert!(matches!(load("x.toml"), Err(E::NotFound { .. })));
}
}
fn main() {
println!("{:?}", load("x.toml"));
}💡 对照:
assert_eq!(err, E::NotFound { .. })更简洁,但要求错误类型实现PartialEq,而io::Error、ParseIntError都没实现PartialEq——所以模式匹配 +matches!才是通用手法:rustassert!(matches!(load("x"), Err(E::NotFound { .. })));
用 clippy::unwrap_used 管控 unwrap
rust
// lib.rs 顶部:整个 crate 禁止 unwrap(测试模块里再放行)
#![deny(clippy::unwrap_used, clippy::expect_used)]rust
// 确实需要的地方,逐处放行并说明理由
#[allow(clippy::unwrap_used)] // 已由上面的 match 保证 Some
fn last_or_default(v: &[i32]) -> i32 {
*v.last().unwrap()
}powershell
cargo clippy --all-targets -- -D warnings作者实测的 clippy 输出:
text
warning: used `unwrap()` on an `Option` value
--> src/main.rs:6:17
|
6 | let _ = v.unwrap();
| ^^^^^^^^^^^^
= help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.98.0/index.html#unwrap_used⚠️ 注意:
unwrap_used属于clippy::restriction组,默认关闭(如果默认开启会淹没所有项目)。必须显式-W/-D打开。测试模块常见的豁免写法是#[cfg(test)] mod tests { #![allow(clippy::unwrap_used)] ... }。
与其他语言的对照
| # | 任务 | 其他语言 | Rust | 为什么 Rust 这样做 |
|---|---|---|---|---|
| 1 | 表示一次失败 | Java throw new IOException();Python raise OSError() | Err(MyError::Io(e)) | 错误是值,类型系统可见 |
| 2 | 捕获/处理 | try { } catch (IOException e) { } | match r { Ok(v) => …, Err(e) => … } | 没有隐式跳转,处理点是显式的 |
| 3 | 向上传播 | 不写 catch 就自动冒泡 | ?(每一层都要写) | 代价是啰嗦,收益是「从签名就知道会失败」 |
| 4 | 栈展开 vs 提前返回 | Throwable 携带栈快照,printStackTrace() | ? 是普通 return,不生成栈快照 | 零运行时成本;要看栈得用 RUST_BACKTRACE + panic |
| 5 | 判断错误类别 | Java instanceof / catch (SpecificException) | match 变体 / e.downcast_ref::<T>() | 枚举变体是编译期穷尽的,downcast 用于类型擦除后 |
| 6 | 在链上找某类错误 | Go errors.Is(err, fs.ErrNotExist) / errors.As(err, &pe) | source() 循环 + downcast_ref(或匹配 io::ErrorKind) | 标准库只给 source(),遍历要自己写;io::ErrorKind 是结构化替代 |
| 7 | 包装并保留原因 | Python raise ConfigError() from e;Java new XException(msg, cause) | #[source]/#[from](thiserror)、anyhow::Context | 原因存在 source(),? 负责 From 转换 |
| 8 | 附加人话上下文 | Go fmt.Errorf("read config: %w", err) | .context("read config") / .map_err(...) | 上下文是独立的一层,可叠加 |
| 9 | 终止性错误 | Java Error(OutOfMemoryError) | panic! / abort | panic 默认不可恢复,但线程边界可 catch_unwind |
| 10 | 资源清理 | Java finally / try-with-resources;Go defer | RAII(Drop),? 提前返回也会执行 | ? 是普通返回,作用域退出即析构 |
| 11 | 忽略错误的难度 | Python except: pass(静默) | let _ = r;(必须显式) | #[must_use] 让「忘记」变成警告 |
| 12 | 在闭包里传播错误 | Python 闭包里 raise 正常;Go 闭包里 return err 正常 | ? 不能用在返回 () 的闭包里;必须让闭包返回 Result,或用 try_for_each / collect | ? 依赖「当前函数/闭包的返回类型」,FnOnce() -> () 无处可返回 |
| 13 | 遍历时遇到错误就停 | Python for x in xs: v = f(x) + 异常 | xs.iter().try_for_each(...)? 或 xs.iter().map(f).collect::<Result<Vec<_>, _>>()? | 同样的类型约束,标准库给了两个惯用出口 |
| 14 | 错误类型可否扩展 | Java 加异常子类即可 | 加 enum 变体 + #[non_exhaustive] | 保护下游 match 不被破坏 |
| 15 | 库的错误类型 | Java 声明 throws IOException;Go 返回 error 接口 | 具名 enum + Error trait | 调用方能 match,也能当 dyn Error 用 |
| 16 | 二进制的错误类型 | 通常用统一异常基类 | anyhow::Error(类型擦除 + 上下文链) | 没有下游消费者,只需清晰诊断 |
重点:#12 —— ? 不能跨闭包
这是「异常 vs Result」差异最扎人的地方。看这段:
rust
fn sum_parsed(lines: &[String]) -> Result<i64, std::num::ParseIntError> {
let nums: Vec<i64> = lines.iter().map(|l| l.parse::<i64>()?).collect();
Ok(nums.iter().sum())
}作者实测的编译错误:
text
error[E0277]: the `?` operator can only be used in a closure that returns `Result` or `Option`
(or another type that implements `FromResidual`)
--> src/main.rs:2:63
|
2 | let nums: Vec<i64> = lines.iter().map(|l| l.parse::<i64>()?).collect();
| --- ^ cannot use the `?` operator in a
| | closure that returns `i64`
| this function should return `Result` or `Option` to accept `?`原因:map 期望 FnMut(&String) -> i64,闭包的返回类型是 i64,而 ? 需要「当前闭包」能返回一个残差类型。? 的提前返回只能作用于它所在的函数或闭包,不能穿越闭包边界传给外层函数——因为闭包有自己的调用约定和返回类型。
Python 里 for + raise 不受此限(异常穿越任何帧),Go 里 for + return 也不受限(for 不是闭包)。Rust 的代价是:想用迭代器风格,就必须换用「整条链产出 Result」的组合子。
三种正确写法:
rust
use std::num::ParseIntError;
fn sum_a(lines: &[String]) -> Result<i64, ParseIntError> {
// 写法 1:collect 到 Result<Vec<_>, _>,遇到第一个 Err 就整体返回 Err
let nums: Vec<i64> = lines.iter().map(|l| l.parse::<i64>()).collect::<Result<_, _>>()?;
Ok(nums.iter().sum())
}
fn sum_b(lines: &[String]) -> Result<i64, ParseIntError> {
// 写法 2:try_for_each,闭包自己返回 Result<T, E>,? 在闭包内部合法
let mut total = 0i64;
lines.iter().try_for_each(|l| {
total += l.parse::<i64>()?;
Ok(())
})?;
Ok(total)
}
fn sum_c(lines: &[String]) -> Result<i64, ParseIntError> {
// 写法 3:让闭包显式返回 Result,再在最外层 ?
let parse = |l: &String| -> Result<i64, ParseIntError> { l.parse() };
let nums: Result<Vec<i64>, _> = lines.iter().map(parse).collect();
Ok(nums?.iter().sum())
}
fn main() {
let lines = vec!["1".to_string(), "2".to_string(), "3".to_string()];
println!("{:?} {:?} {:?}", sum_a(&lines), sum_b(&lines), sum_c(&lines));
let bad = vec!["1".to_string(), "x".to_string()];
println!("{:?}", sum_a(&bad));
}输出:
textOk(6) Ok(6) Ok(6) Err(ParseIntError { kind: InvalidDigit })
🧠 原理:
collect::<Result<Vec<T>, E>>()和collect::<Option<Vec<T>>>()是标准库的专用实现(FromIterator<Result<T, E>> for Result<V, E>):把「一串 Result」折成「一个 Result 套一串值」,遇到第一个Err就短路。这是 Rust 里替代「try 块」的惯用手法。🚀 进阶:如果确实需要「在返回
()的闭包里用?」,可以在闭包内部再开一个返回Result的内部闭包并立刻调用它(IIFE 模式):rustfn outer() -> Result<usize, Box<dyn std::error::Error>> { // 场景:某个接收 FnOnce() -> () 的 API(如 thread::spawn) // 里想用 `?`,就自己包一个返回 Result 的闭包 let r = (|| -> Result<usize, Box<dyn std::error::Error>> { let text = std::fs::read_to_string("no-such-file.txt")?; // ? 在这里合法 Ok(text.lines().count()) })(); r } fn main() { println!("{:?}", outer().is_err()); }输出
true(文件不存在,?把io::Error提前返回给了 IIFE 的返回类型)。这种「立即调用的闭包」在需要?又不想为它单独写一个具名函数时很有用。真正语法层面的try { ... }块(try blocks)目前仍是 unstable,本章不作为主线。
常见坑与编译错误
E0277:? 用在了返回 Result 以外的函数里
rust
fn main() {
let n: i32 = "1".parse()?; // main 返回 ()
println!("{n}");
}text
error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option`
(or another type that implements `FromResidual`)
--> src/main.rs:2:33
|
1 | fn main() {
| --------- this function should return `Result` or `Option` to accept `?`
2 | let n: i32 = "1".parse()?;
| ^ cannot use the `?` operator in a function that returns `()`原因:? 的语义包含 return Err(...),而 () 不能装错误。 修法:fn main() -> Result<(), Box<dyn Error>>(或 anyhow::Result<()>),或在 main 里用 match / unwrap 就地处理。 闭包版:报错文字变成 can only be used in a closure that returns ...,修法见「与其他语言的对照」 #12。
E0277:From 未实现,? 转换不过去
rust
use std::fs;
#[derive(Debug)]
struct AppError(String);
impl From<std::io::Error> for AppError {
fn from(e: std::io::Error) -> Self { AppError(e.to_string()) }
}
fn load_number(path: &str) -> Result<i32, AppError> {
let text = fs::read_to_string(path)?;
let n: i32 = text.trim().parse()?; // 这里炸
Ok(n)
}text
error[E0277]: `?` couldn't convert the error to `AppError`
--> src/main.rs:14:37
|
12 | fn load_number(path: &str) -> Result<i32, AppError> {
| --------------------- expected `AppError` because of this
13 | let text = fs::read_to_string(path)?;
14 | let n: i32 = text.trim().parse()?;
| -------^ the trait `From<ParseIntError>` is not implemented for `AppError`
|
note: `AppError` needs to implement `From<ParseIntError>`
help: the trait `From<ParseIntError>` is not implemented for `AppError`
but trait `From<std::io::Error>` is implemented for it原因:? 只做一步 From::from,每个底层错误类型都要有自己的 From。 修法(三选一):
- 加
impl From<ParseIntError> for AppError { … }(或thiserror的#[from])。 - 就地转换:
.map_err(|e| AppError(e.to_string()))?。 - 改用
anyhow/Box<dyn Error + Send + Sync>,它们对所有E: Error + Send + Sync + 'static都有 blanketFrom。
💡 对照:这相当于 Go 里编译器不让你
return err(类型不匹配error接口)——不,Go 的error是接口所以从不报错;Rust 的E是具体类型,所以必须显式建立转换关系。这是精度换来的啰嗦。
E0308:分支返回类型不一致
rust
fn half(n: i32) -> Result<i32, String> {
match n.checked_rem(2) {
Some(0) => Ok(n / 2),
Some(_) => Err(format!("{n} 是奇数")),
None => n / 2, // 忘了包 Ok
}
}text
error[E0308]: `match` arms have incompatible types
--> src/main.rs:5:17
|
2 | / match n.checked_rem(2) {
3 | | Some(0) => Ok(n / 2),
| | --------- this is found to be of type `Result<i32, String>`
5 | | None => n / 2,
| | ^^^^^ expected `Result<i32, String>`, found `i32`
= note: expected enum `Result<i32, String>`
found type `i32`
help: try wrapping the expression in `Ok`
|
5 | None => Ok(n / 2),原因:match 的所有分支必须同类型。第 1、2 个分支是 Result<i32, String>,第 3 个分支是裸 i32,所以冲突。 修法:按提示包 Ok(...)(None => Ok(n / 2))。
⚠️ 陷阱:不能靠
return n / 2;绕过——函数签名是Result<i32, String>,return的值也必须符合签名。能「无视返回类型」的只有类型为!(never type)的表达式:panic!、todo!、unreachable!、std::process::exit。所以None => todo!()是合法的,None => n / 2不是。
高频变体:Err("字面量") 写进了 Result<_, String>(报 expected String, found &str)→ 加 .to_string(),或把错误类型改成 &'static str / Cow<'static, str>。
unwrap 在 Option 与 Result 上的消息不同
rust
fn main() {
let o: Option<i32> = None;
let _ = o.unwrap();
}text
thread 'main' panicked at src/main.rs:3:13:
called `Option::unwrap()` on a `None` value
note: run with `RUST_BACKTRACE=1` environment variable to display a backtracerust
fn main() {
let r: Result<i32, std::num::ParseIntError> = "x".parse();
let _ = r.unwrap();
}text
thread 'main' panicked at src/main.rs:3:13:
called `Result::unwrap()` on an `Err` value: ParseIntError { kind: InvalidDigit }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace要点:
Option::unwrap只说None,不告诉你「没找到什么」——所以线上日志里裸unwrap几乎没用。Result::unwrap会打印Err的Debug,信息量取决于错误类型的Debug实现(#[derive(Debug)]是底线)。expect("msg")会替换前半句,但Result仍保留: <Debug>后缀。所以消息别写「失败」这种废话,要写清期望与依据。
Box<dyn Error> 不是 Send,跨线程报错
rust
use std::error::Error;
fn might_fail() -> Result<i32, Box<dyn Error>> {
let n: i32 = "12".parse()?;
Ok(n)
}
fn main() {
let h = std::thread::spawn(|| might_fail()); // 这里炸
println!("{:?}", h.join().unwrap());
}text
error[E0277]: `dyn std::error::Error` cannot be sent between threads safely
--> src/main.rs:9:30
|
9 | let h = std::thread::spawn(|| might_fail());
| ^^ `dyn std::error::Error` cannot be sent between threads safely
|
= help: the trait `Send` is not implemented for `dyn std::error::Error`
= note: required because it appears within the type `Box<dyn std::error::Error>`
= note: required because it appears within the type `Result<i32, Box<dyn std::error::Error>>`
= note: required by a bound in `spawn`: `T: Send + 'static`原因:Box<dyn Error> 里可能是 Rc<...> 之类非 Send 的错误,编译器无法保证安全。 修法:把返回类型换成 Box<dyn Error + Send + Sync + 'static>,或者 anyhow::Error(anyhow::Error 内部就要求 Send + Sync)。 连带坑:两者之间没有 From 转换,? 不能把 Box<dyn Error + Send + Sync> 变成 Box<dyn Error>:
text
error[E0277]: `?` couldn't convert the error: `dyn std::error::Error + Send + Sync: Sized` is not satisfied
= note: required for `Box<dyn std::error::Error + Send + Sync>` to implement `std::error::Error`
= note: required for `Box<dyn std::error::Error>` to implement
`From<Box<dyn std::error::Error + Send + Sync>>`→ 一开始就定好统一的装箱类型(推荐 Box<dyn Error + Send + Sync> 或 anyhow::Error)。
thiserror 与 anyhow 混用时类型不兼容
rust
use anyhow::Result;
use thiserror::Error;
#[derive(Debug, Error)]
#[error("配置错误:{0}")]
struct ConfigError(String);
// ❌ 返回 anyhow::Result,却把 ConfigError 直接当 Err 的「类型」用
fn load() -> Result<()> {
let e = ConfigError("缺少 port".into());
let boxed: Box<ConfigError> = Box::new(e);
Err(boxed) // expected `anyhow::Error`, found `Box<ConfigError>`
}原因:anyhow::Error 是具体类型(不是 trait 对象别名)。? 能靠 blanket impl From<E: Error + Send + Sync + 'static> 自动装箱,但手写 Err(...) 不会触发转换。 修法:
rust
use anyhow::Result;
use thiserror::Error;
#[derive(Debug, Error)]
#[error("配置错误:{0}")]
struct ConfigError(String);
fn load() -> Result<()> {
let e = ConfigError("缺少 port".into());
Err(e.into()) // 方式 1:显式 .into()
// 方式 2(更惯用):把可能失败的调用放在被 `?` 修饰的表达式上
// let port = parse_port("abc")?;
}
fn main() {
println!("{:?}", load());
}其他混用坑:
| 坑 | 现象 | 修法 |
|---|---|---|
库的公开 API 用了 anyhow::Error | 下游无法 match,且依赖被强加 | 库返回自定义 enum(thiserror),二进制才用 anyhow |
想从 anyhow::Error 取回具体类型 | downcast_ref::<ConfigError>() 返回 None | 用 e.downcast_ref::<ConfigError>()(对 anyhow 有效,会查整条链);失败说明中间层把它包成了别的类型 |
#[from] 加了两个 | conflicting implementations of From | 每个错误类型最多一个 #[from],其余手写 impl From |
thiserror 1.x 与 2.x 属性差异 | #[error(transparent)] 等写法在 1.x 也可用;2.x 改了部分 #[source] 推断细节 | 统一锁定 thiserror = "2" |
速查表
| 我想…… | 写法 |
|---|---|
| 主动崩溃并留下消息 | panic!("上下文 {x}") |
| 校验不变量 | assert!(cond, "msg {x}") |
| 只在 debug 校验 | debug_assert!(cond) |
| 标记未实现 | todo!("还没写") / unimplemented!("不支持 {mode}") |
| 直接崩溃取错误值 | .unwrap()(仅测试/原型/已证明安全) |
| 崩溃但说明理由 | .expect("配置已在上一步校验") |
| 失败取默认值 | .unwrap_or(0) / .unwrap_or_else(|e| calc(e)) / .unwrap_or_default() |
| 只改成功值 | .map(|v| v * 2) |
| 只改错误值 | .map_err(|e| MyError::from(e)) |
| 成功后再来一步 | .and_then(|v| next(v)) |
| 失败时换条路 | .or_else(|e| fallback(e)) |
| 看一眼成功值(1.76+) | .inspect(|v| tracing::debug!("{v}")) |
| 看一眼错误值(1.76+) | .inspect_err(|e| tracing::warn!("{e}")) |
| 借用而非夺走 | .as_ref() / .as_mut()(Result 只有这两个借用版) |
取走并替换 Result | std::mem::replace(&mut slot, 替代值)(没有 Result::take,也没有 Result: Default) |
取走并留 None(仅 Option) | opt.take() / opt.replace(v) |
转成 Option | .ok() / .err() |
| 传播错误 | let v = f()?; |
| 补一层人话 | .map_err(|e| …)? 或 .context("读取配置")? |
在 Result 函数里用 Option | .ok_or("缺少字段")? / .ok_or_else(|| …)? |
在 Option 函数里用 Result | .ok()? |
| 在 map 闭包里传播 | 用 .collect::<Result<Vec<_>, _>>()? 或 .try_for_each(...)? |
| 只关心失败 | if let Err(e) = r { … } |
| 遍历错误链 | let mut c = e.source(); while let Some(s) = c { …; c = s.source(); } |
| 按类型分支 | e.downcast_ref::<io::Error>() |
| 按 IO 类别分支 | match e.kind() { ErrorKind::NotFound => … } |
| 定义库错误类型 | #[non_exhaustive] pub enum E { … } + Display + Error + From |
| 自动生成 | #[derive(thiserror::Error)] + #[error("…")] / #[from] / #[source] |
| 二进制里加上下文 | use anyhow::{Context, Result}; .context("…")? |
| 立即返回 anyhow 错误 | bail!("msg {x}") / ensure!(cond, "msg") |
main 返回错误 | fn main() -> anyhow::Result<()> |
| 自定义退出码 | fn main() -> std::process::ExitCode |