Skip to content

错误处理工程实践

thiserroranyhow 的分工、错误处理的项目级决策。

工程实践

库 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));
}

输出:

text
1 2

expect() 的消息怎么写

expect / unwrapResultOption 上的 panic 消息不一样,先记住原文(作者实测):

调用panic 消息
opt.unwrap()Nonecalled `Option::unwrap()` on a `None` value
opt.expect("配置文件必须包含端口")配置文件必须包含端口
res.unwrap()Errcalled `Result::unwrap()` on an `Err` value: ParseIntError { kind: InvalidDigit }
res.expect("端口必须是整数")端口必须是整数: ParseIntError { kind: InvalidDigit }

结论:expect 的消息会替换掉「called X::unwrap() on …」那半句,但保留 ErrDebug 输出OptionNone 没有额外信息)。

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::ErrorParseIntError 都没实现 PartialEq——所以模式匹配 + matches! 才是通用手法:

rust
assert!(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 ErrorOutOfMemoryErrorpanic! / abortpanic 默认不可恢复,但线程边界可 catch_unwind
10资源清理Java finally / try-with-resources;Go deferRAII(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));
}

输出:

text
Ok(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 模式):

rust
fn outer() -&gt; Result&lt;usize, Box&lt;dyn std::error::Error&gt;&gt; {
    // 场景:某个接收 FnOnce() -> () 的 API(如 thread::spawn)
    // 里想用 `?`,就自己包一个返回 Result 的闭包
    let r = (|| -&gt; Result&lt;usize, Box&lt;dyn std::error::Error&gt;&gt; {
        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修法(三选一):

  1. impl From<ParseIntError> for AppError { … }(或 thiserror#[from])。
  2. 就地转换:.map_err(|e| AppError(e.to_string()))?
  3. 改用 anyhow / Box<dyn Error + Send + Sync>,它们对所有 E: Error + Send + Sync + 'static 都有 blanket From

💡 对照:这相当于 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>

unwrapOptionResult 上的消息不同

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 backtrace
rust
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 会打印 ErrDebug,信息量取决于错误类型的 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::Erroranyhow::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)。

thiserroranyhow 混用时类型不兼容

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,且依赖被强加库返回自定义 enumthiserror),二进制才用 anyhow
想从 anyhow::Error 取回具体类型downcast_ref::<ConfigError>() 返回 Nonee.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 只有这两个借用版)
取走并替换 Resultstd::mem::replace(&mut slot, 替代值)没有 Result::take,也没有 Result: Default
取走并留 None(仅 Optionopt.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


内容以 rustc 1.98.1 · Rust 2024 edition 为基准