序言
作为一名刚入门 Rust 的开发者,在构建库时,我原本对于 Rust 中错误处理的最佳实践并不了解。在开发 svgr-rs 这个库的过程中,我一开始只是简单地返回 String
来描述错误。然而,一位社区成员在 issue 中提出了建议,应当使用标准的 Error
类型来表示错误。我对此感到困惑,不太明白为何 Error
类型比 String
更恰当。
最近我深入研究了 Rust 官方提供的错误类型处理指南,并对如何在 Rust 中高效且优雅地处理错误有了深入理解。在这篇文章中,我不仅会分享这些指南的要点,还会讲解错误类型的优雅处理方式,并介绍 anyhow
和 thiserror
这两个旨在简化错误处理流程的库。
Rust 错误类型处理指南
在 Rust 库中,当公共函数返回 Result<T, E>
类型时,类型 E
通常被用作错误类型。为了遵循 Rust 的错误处理惯例,错误类型应实现 std::error::Error
trait。这是错误处理库对不同错误类型进行抽象的机制,并且它允许将一个错误用作另一个错误的源( source()
)。
另外,错误类型应实现 Send
和 Sync
特征。一个不是 Send
的错误不能被一个使用 thread::spawn
运行的线程返回。一个不是 Sync
的错误不能使用 Arc
在线程间传递。这些是多线程应用中基本错误处理的常见要求。
Send
和 Sync
对于能够使用 std::io::Error::new
将自定义错误封装进一个 IO 错误也很重要,这需要一个 Error + Send + Sync
的特征约束。
对于这项原则,一个需要保持警惕的地方是在返回错误的函数中,例如 reqwest::Error::get_ref
。通常 Error + Send + Sync + 'static
对于调用者最为有用。添加 'static
生命周期使得特征对象可以和 Error::downcast_ref
一起使用。
永远不要使用 ()
作为错误类型,即使错误没有有用的附加信息可以携带。
()
不实现Error
特征,因此它不能和像error-chain
这样的错误处理库一起使用。()
不实现Display
特征,因此如果用户想要因为错误而失败(fail),他们需要自己编写错误消息。()
对于决定unwrap()
错误的用户来说有一个无用的Debug
表现形式。- 对于下游库来说,实现
From<()>
至它们错误类型并没有语义意义,所以()
作为错误类型不能和?
操作符一起使用。
相反,为你的库或单个函数定义一个有意义的错误类型。提供适当的 Error
和 Display
实现。如果错误没有有用的信息可以携带,它可以作为一个单元结构体来实现。
rust
#![allow(unused)]
fn main() {
use std::error::Error;
use std::fmt::Display;
// 不要这样做...
fn do_the_thing() -> Result<Wow, ()>
// 要这样做...
fn do_the_thing() -> Result<Wow, DoError>
#[derive(Debug)]
struct DoError;
impl Display for DoError { /* ... */ }
impl Error for DoError { /* ... */ }
}
不应该实现 Error::description()
。它已被弃用,用户应始终使用 Display
而不是 description()
来打印错误。
错误信息示例
显示错误类型的错误消息应该是小写的,不带尾标点,并且通常简洁。
- "unexpected end of file"
- "provided string was not
true
orfalse
" - "invalid IP address syntax"
- "second time provided was later than self"
- "invalid UTF-8 sequence of {} bytes from index {}"
- "environment variable was not valid unicode: {:?}"
标准库中的错误类型示例
在 Rust 的标准库中,字符串类型 str
提供了一个 parse
方法,它用于转换字符串到其他类型。例如,当你试图把一个字符串转换成 bool
类型,你需要调用这个 parse
方法,并指定你期望的结果类型为 bool
。如果该字符串不是 "true"
或 "false"
,parse
方法将无法解析它为 bool
类型,并会返回一个错误 std::str::ParseBoolError
。
rust
fn main() {
match "true".parse::<bool>() {
Ok(result) => println!("parse result: {:?}", result),
// 这里的 err 类型为 std::str::ParseBoolError
Err(err) => println!("parse error: {:?}", err)
}
}
std::str::ParseBoolError
代表了一个特定的错误情况:提供的字符串既不等于 "true"
也不等于 "false"
。它是一个没有任何字段的结构体,仅用来表示一个错误的状态。这个结构体实现了基础的 trait Debug
、Clone
、PartialEq
和 Eq
。它还实现了 fmt::Display
trait 的 fmt
方法,当需要时可以打印出友好的错误提示信息。
rust
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseBoolError;
impl fmt::Display for ParseBoolError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
"provided string was not `true` or `false`".fmt(f)
}
}
跟踪和报告错误链
在 Rust 中,std::error::Error
trait 的 source
方法提供了一种途径来跟踪和报告错误链。其工作原理是返回 Option<&(dyn Error + 'static)>
类型,表明在当前错误发生时可能触发它的底层错误("原因"或"源头"),或者如果没有更深层的错误则返回 None
。
标准库(std
)中一个常见的 std::error::Error
trait source
方法的使用示例是处理文件 I/O 操作中可能出现的错误。例如,尝试打开一个不存在的文件会返回 std::io::Error
,我们可以使用 source
方法来获取错误的底层来源。以下是一个例子:
rust
use std::error::Error;
use std::fs::File;
use std::io;
fn open_file(file_path: &str) -> Result<File, io::Error> {
File::open(file_path)
}
fn main() {
match open_file("non_existent_file.txt") {
Ok(_) => println!("File opened successfully."),
Err(e) => {
println!("An error occurred: {}", e);
if let Some(source) = e.source() {
println!("Underlying error: {}", source);
}
}
}
}
在这个例子中,open_file
函数尝试打开一个不存在的文件,返回了一个 std::io::Error
类型的错误。然后我们在主函数中检查这个错误,首先打印基本的错误消息,随后是源错误(source error),也就是导致这个错误的底层系统错误。在 Unix 系统中,可能是因为 "No such file or directory"(没有这样的文件或目录)。在这种情况下,源错误(source)提供了额外的有关底层操作系统错误的信息,source
方法让我们能够访问到这个原始错误。
使用 anyhow
简化应用程序的错误处理
anyhow
是一个 Rust 库,旨在提供灵活的错误处理机制,特别适用于应用程序开发,其中可能需要处理多种错误类型,但不需要编写大量的自定义错误代码。该库的核心是 anyhow::Error
类型,它是一个通用的动态错误类型,能够封装任何实现了 std::error::Error
trait 的错误类型。
以下是 anyhow
的一些主要特点:
- 简化的错误创建和返回 : 使用
anyhow
,你可以通过anyhow!
宏轻松创建错误。它可以接受字符串、字符串字面量,甚至格式化的错误消息。同时,它支持?
运算符用于错误的传递。 - 动态错误类型 :
anyhow
提供的错误类型是动态的,可以封装任意的错误类型。这允许你在不需要详细错误分类或向上游库用户暴露错误类型定义时,快速构建应用程序。 - 链式错误 :
anyhow
支持"链式"错误,这意味着你可以将一个错误附加到另一个错误上,形成一个错误链。这在诊断错误原因上特别有用。 - 无需定义自定义错误类型 : 对于许多应用场景,特别是应用程序开发,不需要定义和维护一整套自己的错误类型。与其他错误处理库(如
thiserror
)不同,anyhow
提供了无需自定义错误类型即可进行错误处理的功能。 - Backtrace 支持 :
anyhow
自动捕获错误发生时的回溯(如果 Rust 已经开启了回溯捕获),这对调试和错误报告至关重要。
一个典型的 anyhow
使用例子如下:
rust
use anyhow::{anyhow, Result};
fn may_fail(flag: bool) -> Result<()> {
if flag {
Ok(())
} else {
Err(anyhow!("An error occurred"))
}
}
fn main() -> Result<()> {
may_fail(false)?;
Ok(())
}
使用 thiserror
简化库中错误的定义和实现
thiserror
是 Rust 中的一个库,旨在简化错误的定义和实现,特别是对 crate 作者在定义需要向使用者公开的自定义错误类型时非常有用。与 anyhow
库相比,thiserror
更适用于库开发场景,它允许创建结构化错误,并为每种错误自动实现了 std::error::Error
trait。
以下是 thiserror
的一些主要特点:
- 结构化错误定义 :
thiserror
提供了一个简洁的宏,用于定义包含错误同一信息和错误原因的错误枚举和结构。 - 自动实现 :
Error
trait 和相关方法(如source()
和fmt()
)的实现会通过thiserror
宏自动完成,避免了编写样板代码。 - 良好的兼容性 : 所定义的错误类型天然支持 Rust 中的错误处理模式,例如使用
?
运算符将错误向上传递。 - 便于使用 : 通过
thiserror
定义的错误可以无缝地集成到库用户的错误处理流程中,同时还保持了 Rust 类型系统带来的类型安全和模式匹配的优势。
一个使用 thiserror
的例子如下:
rust
use thiserror::Error;
// 自定义错误枚举
#[derive(Error, Debug)]
pub enum MyError {
#[error("the data for the key `{0}` is not available")]
NotFound(String),
#[error("the data for the key `{0}` is malformed")]
MalformedData(String),
#[error(transparent)]
Io(#[from] std::io::Error), // 表示从 std::io::Error 转换
// 其他错误变体...
}
fn use_my_error() -> Result<(), MyError> {
// 函数逻辑...
Err(MyError::NotFound("key".to_string()))
}
fn main() {
match use_my_error() {
Ok(_) => println!("Success!"),
Err(e) => println!("An error occurred: {}", e),
}
}
参考资料
- 官方的错误类型处理指南:rust-lang.github.io/api-guideli...
- thiserror 库:github.com/dtolnay/thi...
- anyhow 库:github.com/dtolnay/any...