Rust错误处理的新视角
摘要
本文讨论了Rust中不同的错误处理模式,包括panic、使用Option和Result、以及默认恢复机制,并提出了一种超越简单传播或恢复的错误处理方法。
<p><a href="https://lobste.rs/s/l0yqco/novel_look_at_error_handling_rust">评论</a></p>
查看缓存全文
缓存时间: 2026/07/06 02:00
# 对 Rust 错误处理的新颖看法
来源:https://jtjlehi.github.io/2026/06/25/novel-rust-error-handling.html
关于 Rust 中的错误处理,人们已经说了很多。有大量不同的观点,涉及如何组织错误、何时使用 panic,以及 proc-macros 在错误处理中的作用。大多数关于 Rust 错误处理的讨论都假设:当错误发生时,要么将错误向上传递给调用者,要么尝试完全恢复并继续执行。虽然这描述了许多情况,但并未涵盖所有情况。我想从不同角度看待错误处理以及如何处理它。这并不是要取代主流错误传播的混乱做法,而是希望在您的工具箱中增加一个新工具。
## 现状概述
如果某个函数调用了另一个返回 `Result` 的函数,那么在 Rust 中有几种不同的处理方式。为了说明这一点,假设我们有一个函数,它接受两个字符串,将它们解析为 `u32` 并相加。以下是几种处理解析失败的方式。
它可能直接 panic。在这种情况下,通常会在函数的文档中解释这一点。
```rust
/// 将两个字符串解析为 `u32` 并相加
///
/// # Panics
///
/// 如果提供的字符串无法解析,则 panic
fn add_strs_panic(s1: &str, s2: &str) -> u32 {
let u1 = s1.parse().expect("s1 应该是一个有效的 `u32`");
let u2 = s2.parse().expect("s2 应该是一个有效的 `u32`");
u1 + u2
}
```
它可能返回 `Option`。这种方法通常会利用标准库提供的组合子,如 `Option::ok`,与问号操作符结合使用。
```rust
/// 将两个字符串解析为 `u32` 并相加结果
///
/// 如果解析失败,则返回 `None`
fn add_strs_option(s1: &str, s2: &str) -> Option<u32> {
let u1 = s1.parse().ok()?;
let u2 = s2.parse().ok()?;
Some(u1 + u2)
}
```
它可能返回一个结果类型。这通常涉及以某种方式包装错误。
```rust
enum AddStrsError {
// 以优雅方式包装错误的 variant(为了简洁省略)
}
/// 将两个字符串解析为 `u32` 并相加结果
///
/// 如果解析失败则返回错误
fn add_strs_result(s1: &str, s2: &str) -> Result<u32, AddStrsError> {
let u1 = s1.parse().map_err(/* 为错误提供更多上下文 */)?;
let u2 = s2.parse().map_err(/* 为错误提供更多上下文 */)?;
Ok(u1 + u2)
}
```
甚至还有一种不错的方法来处理完全不失败的情况,而是从失败中恢复。函数可以将错误的输入当作 `0` 或其他随机值处理。(顺便提一句,我认为这种轻松而又干净地从错误中恢复的能力,是“错误即值”的主要好处之一。)
```rust
/// 将两个字符串解析为 `u32` 并相加结果
///
/// 如果解析失败,则将值默认为 0
fn add_strs_default(s1: &str, s2: &str) -> u32 {
// 我们可以使用 `unwrap_or_default`,但我觉得 0 更清晰
let u1 = s1.parse().unwrap_or(0);
let u2 = s2.parse().unwrap_or(0);
u1 + u2
}
```
## 问题所在
最后一个例子有些牵强,对于那个例子可能不太合理,但我确实写过类似这样的代码。“默认值”代码存在一个问题:函数的调用者无法知道是否进行了默认处理。函数从错误中恢复并继续执行是完全合理的。同样,调用者想要知道函数内部发生了某些失败也是合理的。问题在于,当这两个情况同时发生时,缺少一种解决方案。
这种同时发生失败和继续执行的现象比较常见。以下几个(或许更现实的)例子包括:
- 收集编译器在特定阶段发生的所有错误。
- 如果文件路径不存在,则使用默认文件路径。
- 将除法运算符分母为零的情况视为求值结果为 0¹
- 即使某些测试失败了,仍然运行整个测试套件中的所有测试²
## 解决方案 1:Go 方式
很有意思的是,Go 编程语言处理错误的方式并没有这个问题。对于那些不熟悉的人,我会做一个极简的概述。
`Go` 和 Rust 一样,采用“错误即值”。在 Go 中,错误是实现 `Error` 接口的任何类型。与 Rust 不同,`Go` 没有真正的和类型。因此,可以返回一个元组,包含正常路径的值和一个 `error` 类型,如下所示:
```go
func AddStrsDefault(s1 string, s2 string) (uint64, error) {
var err error
i1, err := strconv.ParseUint(s1, 10, 32)
// 注意:如果两个函数都返回错误,只有第二个错误会被看到
i2, err := strconv.ParseUint(s2, 10, 32)
return i1 + i2, err
}
```
由于在 `Go` 中任何指针都可以是 `nil`,如果没有错误,返回的值就设为 `nil`。当我们在 Rust 中遇到上述问题时,这可能是一种可行的方案。如果我们的计算总是会完成,但也可能存在错误,我们可以返回一个可选的错误:
```rust
fn add_strs_default(s1: &str, s2: &str) -> (u32, Option<Error>) {
let mut ret_err: Option<Error> = None;
// 我们本可以用 `unwrap_or_default`,但我觉得 0 更清晰
let u1 = match s1.parse() {
Ok(val) => val,
Err(err) => {
ret_err = Some(/* 映射错误 */);
0
}
};
let u2 = match s2.parse() {
Ok(val) => val,
Err(err) => {
ret_err = Some(/* 映射错误 */);
0
}
};
(u1 + u2, ret_err)
}
```
然后函数的调用者可以选择传播错误,或者直接使用提供的值。需要注意几点:
- 要支持返回多个错误,修改起来很容易。
- 这不如 Go 代码或上述任何变体那样符合人体工程学。主要是因为代码大部分被错误处理代码所消耗。如果这个函数实际上很复杂,我可以想象使用这种模式的函数会变得难以阅读。
- 类型签名并没有真正表明函数可能失败。
我认为对于相对简单的单次函数,这个解决方案是可行的,但如果你的代码中有很多类似函数,或者代码逻辑过于复杂,我认为它解决不了问题。
## 我们真正想要的
为了提出解决方案,我认为有必要列出我们想要的目标。这也有助于判断什么时候需要多个解决方案。针对这个问题,我想要以下几点:
- 应该能够以某种默认值继续工作
- “正常路径”应该尽可能主导逻辑
- 函数的签名应该清楚表明失败的可能性
- 应该容易返回多个错误
- 应该能够与“传统 Rust 错误处理”组合
- 应该能够与自身组合
- 仍然应该能够从函数中提前返回
- 应该尽可能“地道”
## 解决方案 2:错误参数
我提出的第一个解决方案称为“错误参数”。其思想是向可失败函数传递一个参数,这个参数能够以多种不同方式收集错误。这对于那些想要从应用程序的大部分区域收集错误的场景尤其有用,因为这些错误通常是整体性的。创建这样一个东西几乎不需要什么努力,但我认为在生态系统中,像 `anyhow` 这样的 crate 可以作为更全面的 API 来帮助实现这种方法。³
以下是一个简单版本的样子:
```rust
// 你也可以创建一个只保存特定错误的变体。
#[derive(Default)]
struct MultiErrors(Vec<Box<dyn Error>>);
impl MultiErrors {
/// 处理一个 `Result`,将错误内部添加进去
///
/// 这与标准库中的 `Result::ok` 函数非常相似。
// 一个更符合人体工程学的版本可能会以某种形式使用内部可变性
fn handle_result<T, E: Error>(&mut self, res: Result<T, E>) -> Option<T> {
match res {
Ok(val) => Some(val),
Err(err) => {
self.0.push(Box::new(err) as Box<dyn Error>);
None
}
}
}
/// 检查内部是否有任何错误
fn is_ok(&self) -> bool {
self.0.is_empty()
}
/// 获取所有遇到的错误
fn get_errs(&self) -> &[Box<dyn Error>] {
&self.0
}
/// 转换为一个 `Result`
fn as_result(&self) -> Result<(), &[Box<dyn Error>]> {
if self.is_ok() {
Ok(())
} else {
Err(self.get_errs())
}
}
}
```
你可以像这样使用这个类型:
```rust
fn add_strs_result(
s1: &str,
s2: &str,
errs: &mut MultiErrors
) -> u32 {
let u1 = errs.handle_result(s1.parse()).unwrap_or(0);
let u2 = errs.handle_result(s2.parse()).unwrap_or(0);
u1 + u2
}
```
我认为这是一个相当不错的方法。它满足了最初列出的大部分需求。它既能与自身组合,也能与“传统 Rust 错误处理”组合。大大减少了处理单个错误所需的代码量,并且支持多种不同的使用场景。我认为这是该问题的一个非常好的解决方案。
尽管如此,我并不认为这种方法完美无缺。这种方法不强制你处理(或至少承认)失败,它不是世界上最“地道”的方法,返回类型也没有传达该函数可能失败的信息(尽管函数类型整体上传达了)。可能还有其他解决方案,在不同的权衡中做出选择。⁴ 我特别想看看如何利用不稳定的 `Try` trait 来解决这个问题。
## 结论
Rust 中传统的错误处理非常不错。但对于某些类型的任务来说,它还不够充分。具体来说,它不适用于你想继续工作同时又将错误报告给调用者的场景。Go 处理错误的方式可行,但不能与传统错误处理组合。我认为更好的替代方案是将一个 `MultiErrors` 结构体传递给调用者,由它来跟踪函数运行过程中遇到的错误。
希望这能揭示 Rust 错误处理中一个有趣的问题,并引发更多关于如何在 Rust 中处理错误的讨论。
---
¹ https://jtjlehi.github.io/2026/06/25/novel-rust-error-handling.html#fn:1
² https://jtjlehi.github.io/2026/06/25/novel-rust-error-handling.html#fn:2
³ https://jtjlehi.github.io/2026/06/25/novel-rust-error-handling.html#fn:3
⁴ https://jtjlehi.github.io/2026/06/25/novel-rust-error-handling.html#fn:4
相似文章
自定义错误在我的Rust应用中是必不可少的
一篇提倡在Rust应用中使用自定义错误类型的博客文章,解释了如何使用map_err和From trait创建一个统一的AppError枚举,以简化不同子系统间的错误处理。
Rust 中的进行中工作
本文介绍了一种在 Rust 开发过程中延迟错误处理的技术和库,允许开发者临时将错误降级为警告,从而在保持正确性的同时维持生产力。
Rust中的作用域错误
Kan-Ru Chen 介绍了 `scoped-error`,一个新的 Rust crate,旨在通过将上下文附件限定在模块级别来改善错误处理的人机工学,解决了像 anyhow 和 thiserror 等现有 crate 的问题。
Rust - 在 map 闭包中处理 Result
本文探讨了在 map 闭包中使用返回 Result 的函数时常见的 Rust 错误,并提供了多种解决方案:收集结果、使用循环、filter_map 和 try_fold。
Rust中Pretty Printer实现的新设计
一篇博客文章,探讨了Rust中Pretty Printer实现的新设计,解决了将函数式编程研究适应到没有垃圾回收的系统语言中的挑战,并比较了现有的方法,比如'pretty' crate和Oppen风格的Pretty Printer。