错误处理

一句话理解

Rust 把错误分成两类,用类型区分:

  • 可恢复错误 → Result<T, E>,必须显式处理
  • 不可恢复错误 / 违反不变量 → panic!

没有异常、没有隐式传播。? 是唯一的语法糖,它做的是「出错就提前返回,顺便用 From 转换错误类型」。

1. panic 与 Result 的分工

panic!Result<T, E>
语义程序有 bug / 不变量被破坏预期内可能失败的正常路径
行为展开栈(或 abort),进程通常终止返回值,调用方决定
该用在哪数组越界、unwrap 失败、断言文件不存在、网络超时、解析失败、校验不通过
库代码尽量不 panic首选

库代码里 panic 是 API 设计问题

如果你的库函数因为”输入格式不对”而 panic,调用方没法优雅处理。返回 Result 把它变成类型上的可见事实,才是 Rust 的做法。unwrap() / expect() 在库的公开路径上尤其要谨慎。

2. Option 与 Result

enum Option<T> { Some(T), None }
enum Result<T, E> { Ok(T), Err(E) }

Option 表示”可能没有值”,Result 表示”可能失败并带原因”。二者都有丰富的组合子:

方法作用
map / map_err变换成功值 / 错误值
and_then链式调用返回 Option/Result 的函数
ok_or / ok_or_elseOption<T> → Result<T, E>
unwrap_or / unwrap_or_else / unwrap_or_default提供兜底值
is_some / is_ok / is_err判断
?提前返回
fn find_user(id: u64) -> Option<String> { ... }
 
// ❌ 嵌套 match
let name = match find_user(1) {
    Some(n) => n,
    None => "匿名".to_string(),
};
 
// ✅ 组合子
let name = find_user(1).unwrap_or_else(|| "匿名".to_string());

3. ? 运算符

use std::num::ParseIntError;
 
fn double(s: &str) -> Result<i32, ParseIntError> {
    let n: i32 = s.trim().parse()?;   // 出错直接 return Err(e)
    Ok(n * 2)
}

? 的背后是一次 From 转换,等价于:

let n: i32 = match s.trim().parse() {
    Ok(v) => v,
    Err(e) => return Err(From::from(e)),
};

这是 From 最重要的用途:让 ? 能把各种底层错误自动转成你自己定义的错误类型。

? 也能用在返回 Option 的函数里:

fn first_char_len(s: &str) -> Option<usize> {
    let c = s.chars().next()?;   // None 就直接返回 None
    Some(c.len_utf8())
}

? 不会吞掉上下文

但它也不会添加上下文。底层报”文件未找到”时,你往往还想知道”是哪个配置文件”。这时要么用 anyhow 的 .context(),要么在自己的错误枚举里带上路径字段。

4. 自定义错误:两种典型分工

库 / 需要被调用方区分的场合:thiserror

use thiserror::Error;
 
#[derive(Debug, Error)]
pub enum ConfigError {
    #[error("读取配置文件失败: {0}")]
    Io(#[from] std::io::Error),
 
    #[error("解析 JSON 失败: {0}")]
    Parse(#[from] serde_json::Error),
 
    #[error("缺少必填项 `{0}`")]
    MissingField(String),
}

#[from] 自动生成 From 实现,于是 std::io::Error 和 serde_json::Error 都能被 ? 自动转换。调用方可以 match 具体分支做不同处理。

应用 / 只关心”出错了 + 上下文”:anyhow

use anyhow::{Context, Result};
 
fn load_config(path: &str) -> Result<Config> {
    let text = std::fs::read_to_string(path)
        .with_context(|| format!("读取 {path} 失败"))?;
 
    let cfg: Config = serde_json::from_str(&text)
        .context("解析配置失败")?;
 
    Ok(cfg)
}

anyhow::Error 能装下任意实现了 std::error::Error 的类型,并保留错误链。

一句话选型

库的公开 API 用 thiserror(具体错误类型),应用的主流程用 anyhow(装箱 + 上下文)。 不要把 anyhow::Error 暴露在库的公开签名里——那等于告诉调用方”你自己去 downcast 猜吧”。

手写实现 Error

不想引依赖时:

#[derive(Debug)]
struct MyError { msg: String }
 
impl std::fmt::Display for MyError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.msg)
    }
}
 
impl std::error::Error for MyError {}

5. Box<dyn Error>:最省事的兜底

fn run() -> Result<(), Box<dyn std::error::Error>> {
    let n: i32 = "42".parse()?;
    println!("{n}");
    Ok(())
}

main 也可以直接返回它:

fn main() -> Result<(), Box<dyn std::error::Error>> {
    run()?;
    Ok(())
}

需要跨线程传递时加上 Send + Sync:

type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;

什么时候够用

小工具、脚本、一次性程序用 Box<dyn Error> 完全没问题。一旦需要按错误类型分支处理,或者要把错误暴露成库 API,就换回具体类型。

6. unwrap / expect 的边界

let v: Vec<i32> = vec![];
// v[0];                       // panic: index out of bounds
let first = v.first().unwrap(); // panic: called `Option::unwrap()` on a `None` value
 
let n: i32 = "abc".parse().unwrap(); // panic: invalid digit found in string
场合建议
测试代码、示例、原型随便用,能快速暴露问题
已经判断过不可能失败(有注释说明理由)用,但写清为什么
启动阶段读取必需配置可接受(此时失败就该退出)
库的公开路径、请求处理路径不要用,返回 Result
长驻服务的主循环不要用,一个请求不该拖垮进程

expect 比 unwrap 好,因为它会打印你的说明:

let port: u16 = env::var("PORT").expect("必须设置环境变量 PORT").parse()
    .expect("PORT 必须是合法端口号");

7. 什么时候该 panic

真正该 panic 的是程序自身有 bug的情况:

  • 断言失败(assert!、debug_assert!)
  • 违反了类型系统或业务不变量(例如状态机进入不可能状态)
  • 初始化阶段缺了必需依赖,继续跑没有意义

而在长驻服务里,更稳的做法是:

  • 用 Result 一路传上来
  • 在任务边界统一记录日志并决定是否重启该任务
  • 让 panic = "abort" 之外的场合保持栈展开,交给 catch_unwind 兜底(谨慎使用)

8. 常见实践清单

  • 错误类型要能被 match:库用枚举,应用可装箱
  • 错误消息要带上下文:路径、ID、参数值
  • 用 #[from] / From 而不是到处手写 map_err
  • 用 ? 而不是嵌套 match
  • 保留错误链:thiserror 的 #[source]、anyhow 的 context
  • 给错误实现 Debug(Error 要求 Debug + Display)
  • 别用字符串当错误类型——Result<T, String> 会让调用方无法分支

9. 小结

关键判断

  1. 预期内的失败返回 Result,破坏不变量才 panic
  2. ? = 出错提前返回 + From 转换错误类型
  3. 库用 thiserror,应用用 anyhow;Box<dyn Error> 适合小工具
  4. 错误消息要带上下文,错误类型要可 match
  5. unwrap 只留在测试、示例和”确实不可能失败且已注明理由”的地方

相关笔记