Rust 的 std::error::Error trait 是错误处理生态的基石。所有可被当作"错误"使用的类型都需要实现这个 trait,它让不同来源的错误能统一传播、展示和追溯。
1、Error trait 定义
rust
pub trait Error: Debug + Display {
fn source(&self) -> Option<&(dyn Error + 'static)> { None }
fn description(&self) -> &str { ... } // 已弃用,直接使用 Display
fn cause(&self) -> Option<&dyn Error> { ... } // 已弃用,被 source() 取代
fn provide<'a>(&'a self, request: &mut Request<'a>) { ... } // 实验性方法,极少用到
}
fn source(&self) -> Option<&(dyn Error + 'static)>
-
默认返回
None,表示当前错误没有更底层的来源。 -
当一个高阶模块的错误是由底层模块的错误引起时,我们会重写这个方法,把底层的错误返回出去。这样外部代码就可以通过不断调用
source()来遍历整个错误链,一直追溯到最根本的原因。 -
例如:你的自定义错误包含一个
std::io::Error,那么source()就应该返回这个io::Error的引用。 -
这是错误溯源(error chaining)的关键,也是 Rust 错误处理优雅的地方。
- 任何实现 Error 的类型都必须同时实现 Debug 和 Display;
Debug:面向开发者,输出调试细节Display:面向用户,给出小写、无标点的简洁描述,例如"字符串中发现无效数字"
- 如果你的错误包装了其他错误,就重写
source()来返回它; - 忽略
description()和cause(),它们只是过时的历史; provide()则留待未来特性稳定后再使用。
2、为什么需要自定义错误类型
直接用 String 或 Box<dyn Error> 虽然方便,但会丢失结构化信息,导致:
- 无法区分错误类别(IO 错误、解析错误、业务错误)
- 调用方无法按类型进行恢复或分支处理
- 不能携带上下文数据(如文件名、行号)
- 不利于日志收集和测试验证
因此,实际项目通常会定义自定义错误枚举或结构体 ,并为其实现 Error trait,同时配合 From 转换以利用 ? 操作符自动传播错误。
3、手动实现 Error trait
(1)自定义错误
rust
use std::fmt;
// 自定义错误枚举
#[derive(Debug)]
enum MyError {
ErrA(String),
ErrB(String),
}
/// 实现 fmt::Display trait
/// 将错误转换为可读的字符串表示
impl fmt::Display for MyError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
MyError::ErrA(msg) => write!(f, "ErrA错误: {}!", msg),
MyError::ErrB(msg) => write!(f, "ErrB错误: {}!", msg),
}
}
}
/// 成功返回 i32,失败返回 MyError
fn my_fn(i: i32) -> Result<i32, MyError> {
match i {
1 => Err(MyError::ErrA("不能是1".to_string())),
2 => Err(MyError::ErrB("不能是2".to_string())),
_ => Ok(i),
}
}
fn main() {
match my_fn(1) {
Ok(i) => println!("成功: {}", i),
Err(e) => eprintln!("失败: {}", e), // 失败: ErrA错误: 不能是1!
}
match my_fn(2) {
Ok(i) => println!("成功: {}", i),
Err(e) => eprintln!("失败: {}", e), // 失败: ErrB错误: 不能是2!
}
match my_fn(3) {
Ok(i) => println!("成功: {}", i), // 成功: 3
Err(e) => eprintln!("失败: {}", e),
}
println!("{:?}", my_fn(2)); // Err(ErrB("不能是2"))
println!("{:?}", my_fn(3)); // Ok(3)
}
(2)自定义错误装箱为 Box<dyn Error>
rust
use std::fmt;
// 自定义错误枚举
#[derive(Debug)]
enum MyError {
ErrA(String),
ErrB(String),
}
/// 实现 fmt::Display trait
/// 将错误转换为可读的字符串表示
impl fmt::Display for MyError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
MyError::ErrA(msg) => write!(f, "ErrA错误: {}!", msg),
MyError::ErrB(msg) => write!(f, "ErrB错误: {}!", msg),
}
}
}
/// MyError 实现 std::error::Error trait
/// 这使得 MyError 可以作为通用错误类型使用(通过 Box<dyn Error>)
impl Error for MyError {}
/// 返回值: Result<i32, Box<dyn Error>> - 成功返回 i32,失败返回装箱的错误对象
fn my_fn(i: i32) -> Result<i32, Box<dyn Error>> {
match i {
1 => Err(Box::new(MyError::ErrA("不能是1".to_string()))),
2 => Err(Box::new(MyError::ErrB("不能是2".to_string()))),
_ => Ok(i),
}
}
fn main() {
match my_fn(1) {
Ok(i) => println!("成功: {}", i),
Err(e) => eprintln!("失败: {}", e), // 失败: ErrA错误: 不能是1!
}
match my_fn(2) {
Ok(i) => println!("成功: {}", i),
Err(e) => eprintln!("失败: {}", e), // 失败: ErrB错误: 不能是2!
}
match my_fn(3) {
Ok(i) => println!("成功: {}", i), // 成功: 3
Err(e) => eprintln!("失败: {}", e),
}
println!("{:?}", my_fn(2)); // Err(ErrB("不能是2"))
println!("{:?}", my_fn(3)); // Ok(3)
}
(3)实现底层错误传递 source()
rust
use std::fmt;
use std::error::Error;
// --- 1. 定义底层错误 (Custom Error 2) ---
#[derive(Debug)]
struct ErrorB {
msg: String,
}
impl fmt::Display for ErrorB {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "底层错误 B: {}", self.msg)
}
}
// ErrorB 没有更底层的错误,所以 source() 使用默认实现 (返回 None)
impl Error for ErrorB {}
// --- 2. 定义上层错误 (Custom Error 1),它包含 ErrorB ---
#[derive(Debug)]
struct ErrorA {
context: String,
source_error: ErrorB, // 持有底层错误
}
impl fmt::Display for ErrorA {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "上层错误 A : {}", self.context)
}
}
impl Error for ErrorA {
// 关键:实现 source() 方法,返回对内部 ErrorB 的引用
fn source(&self) -> Option<&(dyn Error + 'static)> {
Some(&self.source_error)
}
}
fn main() {
// 模拟产生一个底层错误 B
let err_b = ErrorB {
msg: "B数据库连接超时".to_string(),
};
// 将 B 包装进上层错误 A
let err_a = ErrorA {
context: "A用户登录失败".to_string(),
source_error: err_b,
};
// 打印上层错误A
println!("直接打印 ErrorA: {}", err_a);
// 通过 source() 链式获取底层错误
if let Some(source) = err_a.source() {
println!("通过 source() 获取到的底层错误: {}", source);
// 验证底层错误是否还有更底层的错误
if let Some(deep_source) = source.source() {
println!("更深层的错误: {}", deep_source);
} else {
println!("底层错误已无更多来源.");
}
}
// 循环获取所有底层错误,不显示上层错误
let mut source = err_a.source(); // 从上层错误 A 开始
while let Some(e) =source { // 循环直到 source() 返回 None
println!("{}", e); // 打印当前错误
source = e.source(); // 递归获取更底层的错误
}
}
程序输出:
bash
直接打印 ErrorA: 上层错误 A : A用户登录失败
通过 source() 获取到的底层错误: 底层错误 B: B数据库连接超时
底层错误已无更多来源.
底层错误 B: B数据库连接超时
4、实现 From 自动转换(方便使用 ? )
rust
#[derive(Debug)]
struct ErrorB {
msgb: String,
}
#[derive(Debug)]
struct ErrorA {
msga: String,
}
// 将 B 错误包装成 A 错误
impl From<ErrorB> for ErrorA {
fn from(err: ErrorB) -> Self {
ErrorA{
msga: err.msgb,
}
}
}
// 使用 ? 转换错误
fn test_error_a()-> Result<(), ErrorA> {
Err(ErrorB {msgb: "B发生错误".to_string()})?
}
fn main() {
let e = test_error_a();
println!("{:?}", e);
}
// 程序输出:Err(ErrorA { msga: "B发生错误" })
5、错误处理库
thiserror库:https://crates.io/crates/thiserroranyhow库:https://crates.io/crates/anyhow
6、总结
- (1)用枚举区分错误种类:让调用方能进行模式匹配并采取不同恢复策略。
- (2)实现
source()构建错误链:保留根因,方便调试和日志追踪。 - (3)**配合
From与?**:减少显式map_err,让代码更流畅。 - (4)用
thiserror降低样板代码:在生产项目中广泛使用,安全且高效。 - (5)**不随意使用
unwrap或expect**:除非在测试代码或确定不会失败的场景(如配置加载失败直接 panic),否则应始终通过Result传播错误。 - (6)避免在公共 API 中暴露内部实现细节:错误消息应面向用户或调用者,而不是泄露内部路径或变量名,除非用于调试日志。
- (7)考虑使用
anyhow处理应用层错误 :对于不需要精细分类的应用程序顶层错误(如 main 函数或 CLI 工具),anyhow::Error提供了极大的便利性,它自动实现了From转换并支持上下文附加。