错误处理
一句话理解
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_else | Option<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. 小结
关键判断
- 预期内的失败返回
Result,破坏不变量才 panic?= 出错提前返回 +From转换错误类型- 库用
thiserror,应用用anyhow;Box<dyn Error>适合小工具- 错误消息要带上下文,错误类型要可
matchunwrap只留在测试、示例和”确实不可能失败且已注明理由”的地方