
Rust 的错误处理机制以安全性和明确性为核心,通过 panic
和 Result
双轨制应对不同场景。panic
处理不可恢复的致命错误,而 Result
则优雅管理可预测异常。本文将深入解析其设计哲学、?
运算符的魔法、自定义错误实践,并探讨 thiserror
/ anyhow
库如何简化开发,助你构建健壮的 Rust 程序。
Panic
Rust 通过 panic
机制来处理致命错误。
如果在运行时发生致命错误,Rust 将触发 panic
:
Rust
fn main() {
let v = vec![10, 20, 30];
dbg!(v[100]);
}
panic
会直接导致程序退出,用于处理不可恢复且意外的错误。
panic
是程序中存在漏洞的迹象。- 运行时失败(如越界检查失败)可能会引发
panic
。 - 断言(如
assert!
)失败时会引发panic
。 - 特定目的的
panic
可以使用panic!
宏。
panic
会展开栈,上述代码会打印如下的栈信息:

如果程序不允许崩溃,请使用不会引发 panic
的 API (如 Vec::get
)。
Rust
fn main() {
let v = vec![10, 20, 30];
println!("{}", v.get(100).unwrap_or(&-1));
println!("{}", v.get(1).unwrap_or(&-1));
}
默认情况下,一次 panic
会导致栈展开。栈展开过程可以被捕获:
Rust
use std::panic;
fn main() {
let result = panic::catch_unwind(|| "No problem here!");
dbg!(result);
let result = panic::catch_unwind(|| {
panic!("oh no!");
});
dbg!(result);
}
捕获 panic
并不常见;不要尝试使用 catch_unwind
来实现异常处理!
这在服务器中可能很有用,即使单个请求崩溃,服务器也应继续运行。
如果在你的 Cargo.toml
文件中设置了 panic = 'abort'
,则捕获 panic
就不起作用了。
Result

在 Rust 中,我们用于错误处理的主要机制是 Result
枚举,我们在讨论标准库时曾简要提及过它。
Rust
use std::fs::File;
use std::io::Read;
fn main() {
let file: Result<File, std::io::Error> = File::open("diary.txt");
match file {
Ok(mut file) => {
let mut contents = String::new();
if let Ok(bytes) = file.read_to_string(&mut contents) {
println!("Dear diary: {contents} ({bytes} bytes)");
} else {
println!("Could not read file content");
}
}
Err(err) => {
println!("The diary could not be opened: {err}");
}
}
}
Result
有两个成员:Ok
,它包含成功值;Err
,它包含某种类型的错误值。
一个函数是否产生错误可以通过函数返回 Result
来判断。
与 Option
类似,Result
不存在忘记处理错误的情况:在对 Result
进行模式匹配以检查属于哪个成员之前,你无法访问到成功值或错误值。
像 unwrap
这样的方法使得编写简单但不健壮的的代码变得更容易,使用 unwrap()
或 expect()
可能隐藏错误,但这也意味着在你的源代码中,你始终能够看出在哪些地方跳过了错误处理。
一些语言使用异常,例如 C++ 、Java 、Python。
在支持异常的语言中,一个函数是否会抛出异常并不会作为其类型签名的一部分显现出来。这通常意味着在调用一个函数时,你无法判断它是否可能会抛出异常。
异常通常会展开调用栈,向上传播,直到到达一个 try
块。一个源自调用栈深处的错误可能会影响到更上层一个不相关的函数。
一些语言让函数在返回成功值的同时返回一个错误码(或其他某种错误值)。比如 C 和 Go。
根据不同语言的情况,有可能会忘记检查错误值,如果这样,您可能正在访问一个未初始化或无效的成功值。
Rust 使用了另一种方案------Result
,当一个函数返回 Result
时,开发者就会立即知道这个函数可能会返回一个错误,开发者无法简单的忽略 Result
直接获取正确的值,必须处理 Result
。要么使用模式匹配,要么使用类似 unwrap_xxx
的函数获取正确的值。
? 操作符
诸如连接被拒绝或文件未找到之类的运行时错误使用 Result
类型进行处理,但每次调用时都匹配该类型可能会很麻烦。?
操作符用于将错误返回给调用者。它能让你把常见的情况转化为简单得多的:
Rust
match some_expression {
Ok(value) => value,
Err(err) => return Err(err),
}
上述代码可以使用更加简单的形式:
Rust
some_expression?
我们可以用这个来简化我们的错误处理代码:
Rust
use std::io::Read;
use std::{fs, io};
fn read_username(path: &str) -> Result<String, io::Error> {
let username_file_result = fs::File::open(path);
let mut username_file = match username_file_result {
Ok(file) => file,
Err(err) => return Err(err),
};
let mut username = String::new();
match username_file.read_to_string(&mut username) {
Ok(_) => Ok(username),
Err(err) => Err(err),
}
}
简化后的代码:
Rust
use std::io::Read;
use std::{fs, io};
fn read_username(path: &str) -> Result<String, io::Error> {
let mut username_file = fs::File::open(path)?;
let mut username = String::new();
username_file.read_to_string(&mut username)?;
Ok(username)
}
很简洁,不是吗?
实际上,?
的展开比之前所讲的要更复杂一点:
Rust
expression?
会展开为:
Rust
match expression {
Ok(value) => value,
Err(err) => return Err(From::from(err)),
}
这里的 From::from
调用意味着我们试图将错误类型转换为函数返回的类型。这使得将错误封装到更高级别的错误中变得更容易。
例如:
Rust
use std::error::Error;
use std::io::Read;
use std::{fmt, fs, io};
#[derive(Debug)]
enum ReadUsernameError {
IoError(io::Error),
EmptyUsername(String),
}
impl Error for ReadUsernameError {}
impl fmt::Display for ReadUsernameError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::IoError(e) => write!(f, "I/O error: {e}"),
Self::EmptyUsername(path) => write!(f, "Found no username in {path}"),
}
}
}
impl From<io::Error> for ReadUsernameError {
fn from(err: io::Error) -> Self {
Self::IoError(err)
}
}
fn read_username(path: &str) -> Result<String, ReadUsernameError> {
let mut username = String::with_capacity(100);
fs::File::open(path)?.read_to_string(&mut username)?; // 注意这里使用了简化的 ?. 链式调用
if username.is_empty() {
return Err(ReadUsernameError::EmptyUsername(String::from(path)));
}
Ok(username)
}
fn main() {
//std::fs::write("config.dat", "").unwrap();
let username = read_username("config.dat");
println!("username or error: {username:?}");
}
?
运算符必须返回与函数返回类型兼容的值。对于 Result
来说,这意味着错误类型必须兼容。一个返回 Result<T, ErrorOuter>
的函数,只有当 ErrorOuter
和 ErrorInner
是相同类型,或者 ErrorOuter
实现了 From<ErrorInner>
时,才能对 Result<U, ErrorInner>
类型的值使用 ?
。
上述代码中,实现了 impl From<io::Error> for ReadUsernameError
,所以可以直接使用 fs::File::open(path)?.read_to_string
。
实现 From
的一个常见替代方法是使用 Result::map_err
,特别是当转换只在一个地方进行时。
对于 Option
没有兼容性要求。一个返回 Option<T>
的函数可以对任意 T
和 U
类型的 Option<U>
使用 ?
运算符。
返回 Result
的函数不能对 Option
使用 ?
,反之亦然。不过,Option::ok_or
可以将 Option
转换为 Result
,而 Result::ok
可以将 Result
转换为 Option
。
Rust
let op = Option::Some(10);
let res = op.ok_or(ParseError);
let res: Result<i32, ParseError> = Ok(10);
let op = res.ok();
动态 Error
有时候,我们希望允许返回任何类型的错误,而无需编写涵盖所有不同可能性的自定义枚举。std::error::Error
trait
使得创建一个可以包含任何错误的 trait
对象变得很容易:
Rust
use std::error::Error;
use std::fs;
use std::io::Read;
fn read_count(path: &str) -> Result<i32, Box<dyn Error>> {
let mut count_str = String::new();
fs::File::open(path)?.read_to_string(&mut count_str)?;
let count: i32 = count_str.parse()?;
Ok(count)
}
fn main() {
fs::write("count.dat", "1i3").unwrap();
match read_count("count.dat") {
Ok(count) => println!("Count: {count}"),
Err(err) => println!("Error: {err}"),
}
}
read_count
函数可能会返回 std::io::Error
(源自文件操作) 或 std::num::ParseIntError
(源自 String::parse
)。
将错误放入 Box
虽然能减少代码量,但会丧失在程序中对不同错误情况进行清晰处理的能力。因此,在库的公共 API 中使用 Box<dyn Error>
通常不是一个好主意,但在一个仅希望在某处显示错误消息的程序中,它可能是一个不错的选择。
在定义自定义错误类型时,请确保实现 std::error::Error
trait
,这样它才能被放入 Box<dyn Error>
。
thiserror
开源库 thiserror 提供了一些宏,有助于在定义错误类型时避免编写冗长的样板代码。它提供了一些派生宏,可帮助实现 From<T>
、Display
和 Error
trait
。
使用 thiserror
,需要在 Cargo.toml 中添加:
Toml
[dependencies]
thiserror = "xxx"
Rust
use std::io::Read;
use std::{fs, io};
use thiserror::Error;
#[derive(Debug, Error)]
enum ReadUsernameError {
#[error("I/O error: {0}")]
IoError(#[from] io::Error),
#[error("Found no username in {0}")]
EmptyUsername(String),
}
fn read_username(path: &str) -> Result<String, ReadUsernameError> {
let mut username = String::with_capacity(100);
fs::File::open(path)?.read_to_string(&mut username)?;
if username.is_empty() {
return Err(ReadUsernameError::EmptyUsername(String::from(path)));
}
Ok(username)
}
fn main() {
//fs::write("config.dat", "").unwrap();
match read_username("config.dat") {
Ok(username) => println!("Username: {username}"),
Err(err) => println!("Error: {err:?}"),
}
}
// Output
// Error: IoError(Os { code: 2, kind: NotFound, message: "No such file or directory" })
Error
派生宏由 thiserror
提供,它有许多有用的属性,有助于以紧凑的方式定义错误类型。
#[error]
中的消息用于派生 Display
trait
。
请注意,thiserror::Error
派生宏虽然具有实现 std::error::Error
trait
的效果,但不是同一个类型;该 trait
和宏不属于同一个命名空间。
anyhow
开源库 anyhow 提供了一种功能丰富的错误类型,支持携带额外的上下文信息,可用于为程序出错前的操作提供语义化的追踪信息。
这可以与 thiserror
中的便捷宏结合使用,从而避免为自定义错误类型显式编写 trait
实现。
Rust
use anyhow::{Context, Result, bail};
use std::fs;
use std::io::Read;
use thiserror::Error;
#[derive(Clone, Debug, Eq, Error, PartialEq)]
#[error("Found no username in {0}")]
struct EmptyUsernameError(String);
fn read_username(path: &str) -> Result<String> {
let mut username = String::with_capacity(100);
fs::File::open(path)
.with_context(|| format!("Failed to open {path}"))?
.read_to_string(&mut username)
.context("Failed to read")?;
if username.is_empty() {
bail!(EmptyUsernameError(path.to_string()));
}
Ok(username)
}
fn main() {
//fs::write("config.dat", "").unwrap();
match read_username("config.dat") {
Ok(username) => println!("Username: {username}"),
Err(err) => println!("Error: {err:?}"),
}
}
anyhow::Error
本质上是对 Box<dyn Error>
的封装。因此,对于库的公共 API 来说,它可能不是一个好的选择,但在应用程序中被广泛使用。
anyhow::Result<V>
是 Result<V, anyhow::Error>
的类型别名。
anyhow::Context
是为标准的 Result
和 Option
类型实现的一个 trait
。要在这些类型上使用 .context()
和 .with_context()
,需要导入 use anyhow::Context
,该方法会使用额外的上下文包装错误值。