目录
- Rust std::error::Error 详解
- 一、Error trait 的定义
- 二、Error: Debug + Display 是什么意思
- 三、Debug、Display、Error 分别负责什么
- 四、为什么 impl Error for ConfigError {} 可以是空的
- 五、最简单的自定义错误
- 六、带字段的自定义错误
- 七、Error::source() 的作用
- 八、错误链
- 九、不要在 Display 和 source() 中重复输出底层错误
- 十、使用枚举统一多个错误
- 十一、Box<dyn Error> 是什么
- 十二、Box<dyn Error + Send + Sync> 是什么
- 十三、dyn Error 的向下转型
- 十四、'static 是什么意思
- 十五、Error 和 Result 的关系
- 十六、错误类型一定要实现 Error 吗
- 十七、常见错误
- 十八、std::error::Error、thiserror 和 anyhow
- 十九、完整实战示例
- 二十、总结
Rust std::error::Error 详解
在 Rust 中,std::error::Error 是用于表示“错误类型”的标准 trait。
它主要解决以下问题:
- 让自定义类型成为标准意义上的错误类型
- 统一不同错误类型的处理方式
- 支持错误原因链
source() - 支持
Box<dyn Error>动态错误 - 让第三方错误处理库识别你的错误类型
需要特别注意:
Result<T, E>中的E并不强制实现std::error::Error。
下面完全合法:
fn test() -> Result<(), String> {
Err("发生错误".to_string())
}
但是,如果希望错误类型能够进入 Rust 标准错误处理体系,通常应该实现 std::error::Error。
Rust 官方将 Error 定义为错误值应满足的基本规范,并要求实现者同时实现 Debug 和 Display。(Rust 文档)
一、Error trait 的定义
简化后的定义如下:
pub trait Error: Debug + Display {
fn source(&self) -> Option<&(dyn Error + 'static)> {
None
}
// 其他带默认实现的方法
}
完整定义大致如下:
pub trait Error: Debug + Display {
fn source(&self) -> Option<&(dyn Error + 'static)>;
fn description(&self) -> &str;
fn cause(&self) -> Option<&dyn Error>;
fn provide<'a>(&'a self, request: &mut Request<'a>);
}
不过,这些方法都有默认实现。当前实际开发中,最常用、最重要的是:
fn source(&self) -> Option<&(dyn Error + 'static)>
其中:
description()已废弃,应该使用Display或to_string()cause()已废弃,应该使用source()provide()目前仍是 nightly 实验性功能source()用于返回底层错误原因
(Rust 文档)
二、Error: Debug + Display 是什么意思
下面这段定义:
pub trait Error: Debug + Display
表示:
任何实现了
Error的类型,都必须同时实现Debug和Display。
也就是说,下面的代码不能直接编译:
struct ConfigError;
impl std::error::Error for ConfigError {}
因为 ConfigError 没有实现:
std::fmt::Debug std::fmt::Display
正确写法是:
use std::error::Error;
use std::fmt;
#[derive(Debug)]
struct ConfigError;
impl fmt::Display for ConfigError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "配置文件错误")
}
}
impl Error for ConfigError {}
三、Debug、Display、Error 分别负责什么
可以简单理解为:
| Trait | 作用 | 常见输出方式 |
|---|---|---|
Debug |
面向开发者,输出调试信息 | {:?}、{:#?} |
Display |
面向用户,输出可读的错误信息 | {} |
Error |
声明该类型是标准错误,并支持错误链等能力 | source() |
例如:
println!("{:?}", error);
println!("{}", error);
假设错误类型如下:
#[derive(Debug)]
struct ConfigError {
filename: String,
}
那么:
println!("{:?}", error);
可能输出:
ConfigError { filename: "app.toml" }
而:
println!("{}", error);
可能输出:
无法读取配置文件 app.toml
因此:
Debug更适合程序员排查问题Display更适合展示给最终用户Error负责把这个类型接入标准错误体系
四、为什么 impl Error for ConfigError {} 可以是空的
你可能经常看到下面的代码:
impl std::error::Error for ConfigError {}
大括号里面什么都没有。
这是因为 Error trait 中的方法都有默认实现。
例如,source() 的默认行为相当于:
fn source(&self) -> Option<&(dyn Error + 'static)> {
None
}
因此:
impl Error for ConfigError {}
表达的意思是:
ConfigError是一个标准错误类型,但它没有需要额外报告的底层错误原因。
Rust 的规则是:实现 trait 时,必须实现所有没有默认实现的方法;已经有默认实现的方法可以不重写。(Rust 文档)
五、最简单的自定义错误
下面定义一个“用户名不能为空”的错误。
use std::error::Error;
use std::fmt;
#[derive(Debug)]
struct EmptyUsernameError;
impl fmt::Display for EmptyUsernameError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "用户名不能为空")
}
}
impl Error for EmptyUsernameError {}
fn validate_username(username: &str) -> Result<(), EmptyUsernameError> {
if username.trim().is_empty() {
return Err(EmptyUsernameError);
}
Ok(())
}
fn main() {
match validate_username("") {
Ok(()) => println!("用户名合法"),
Err(error) => {
println!("用户提示:{}", error);
println!("调试信息:{:?}", error);
}
}
}
输出:
用户提示:用户名不能为空 调试信息:EmptyUsernameError
这里的:
impl Error for EmptyUsernameError {}
是空实现,因为这个错误没有包装其他错误。
六、带字段的自定义错误
实际项目中的错误通常需要保存上下文信息。
例如:
use std::error::Error;
use std::fmt;
#[derive(Debug)]
struct ConfigError {
filename: String,
message: String,
}
impl fmt::Display for ConfigError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"配置文件 `{}` 加载失败:{}",
self.filename,
self.message
)
}
}
impl Error for ConfigError {}
fn load_config() -> Result<(), ConfigError> {
Err(ConfigError {
filename: "app.toml".to_string(),
message: "文件不存在".to_string(),
})
}
fn main() {
if let Err(error) = load_config() {
println!("{}", error);
}
}
输出:
配置文件 `app.toml` 加载失败:文件不存在
这种方式比单纯返回字符串更好,因为错误中可以保存结构化信息:
error.filename error.message
Rust 官方示例也建议错误类型保存具体上下文,而不是只返回一段无法进一步分析的字符串。(Rust 文档)
七、Error::source() 的作用
source() 用于返回:
导致当前错误的底层错误。
例如:
- 程序读取配置文件
- 配置文件读取成功
- 但是端口字段不是数字
- 底层产生
ParseIntError - 上层将其包装为
ConfigError
错误关系可能是:
ConfigError
└── ParseIntError
其中:
ConfigError是上层业务错误ParseIntError是底层原始错误
Rust 官方建议在错误跨越模块或抽象边界时,通过 source() 暴露底层原因。(Rust 文档)
完整示例
use std::error::Error;
use std::fmt;
use std::num::ParseIntError;
#[derive(Debug)]
struct ConfigError {
key: String,
source: ParseIntError,
}
impl fmt::Display for ConfigError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "配置项 `{}` 的值无效", self.key)
}
}
impl Error for ConfigError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
Some(&self.source)
}
}
fn parse_port(value: &str) -> Result<u16, ConfigError> {
value.parse::<u16>().map_err(|source| ConfigError {
key: "server.port".to_string(),
source,
})
}
fn main() {
match parse_port("abc") {
Ok(port) => {
println!("端口号:{}", port);
}
Err(error) => {
println!("错误:{}", error);
if let Some(source) = error.source() {
println!("底层原因:{}", source);
}
}
}
}
输出类似:
错误:配置项 `server.port` 的值无效 底层原因:invalid digit found in string
为什么返回的是引用
source() 的签名是:
fn source(&self) -> Option<&(dyn Error + 'static)>
而不是:
fn source(&self) -> Option<Box<dyn Error>>
因为错误对象已经拥有底层错误:
struct ConfigError {
source: ParseIntError,
}
调用 source() 时只需要借用它:
Some(&self.source)
不需要转移所有权,也不需要重新分配内存。
八、错误链
一个错误还可以继续包装另一个错误,形成错误链:
ApplicationError
└── ConfigError
└── ParseIntError
例如:
use std::error::Error;
use std::fmt;
use std::num::ParseIntError;
#[derive(Debug)]
struct ConfigError {
source: ParseIntError,
}
impl fmt::Display for ConfigError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "配置内容解析失败")
}
}
impl Error for ConfigError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
Some(&self.source)
}
}
#[derive(Debug)]
struct ApplicationError {
source: ConfigError,
}
impl fmt::Display for ApplicationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "应用程序启动失败")
}
}
impl Error for ApplicationError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
Some(&self.source)
}
}
遍历错误链:
fn print_error_chain(error: &dyn Error) {
eprintln!("错误:{}", error);
let mut current = error.source();
while let Some(source) = current {
eprintln!("由此引起:{}", source);
current = source.source();
}
}
使用:
fn main() {
let parse_error = "abc".parse::<u16>().unwrap_err();
let config_error = ConfigError {
source: parse_error,
};
let application_error = ApplicationError {
source: config_error,
};
print_error_chain(&application_error);
}
输出类似:
错误:应用程序启动失败 由此引起:配置内容解析失败 由此引起:invalid digit found in string
九、不要在 Display 和 source() 中重复输出底层错误
假设当前错误包装了底层错误。
不推荐这样写:
impl fmt::Display for ConfigError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "配置解析失败:{}", self.source)
}
}
impl Error for ConfigError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
Some(&self.source)
}
}
因为错误报告程序可能先输出:
配置解析失败:invalid digit found in string
然后继续读取 source(),再次输出:
Caused by:
invalid digit found in string
造成重复。
更推荐:
impl fmt::Display for ConfigError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "配置解析失败")
}
}
底层错误交给:
source()
Rust 官方文档也建议:被包装的底层错误应通过 source() 返回,或者包含在外层错误的 Display 中,通常不要两者同时使用。(Rust 文档)
十、使用枚举统一多个错误
一个函数可能产生多种错误:
- 文件读取错误
- 数字解析错误
- 业务校验错误
可以使用枚举统一表示。
use std::error::Error;
use std::fmt;
use std::io;
use std::num::ParseIntError;
#[derive(Debug)]
enum AppError {
Io(io::Error),
Parse(ParseIntError),
InvalidPort(u16),
}
实现 Display:
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AppError::Io(_) => {
write!(f, "读取配置文件失败")
}
AppError::Parse(_) => {
write!(f, "端口号解析失败")
}
AppError::InvalidPort(port) => {
write!(f, "端口号无效:{}", port)
}
}
}
}
实现 Error:
impl Error for AppError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
AppError::Io(error) => Some(error),
AppError::Parse(error) => Some(error),
AppError::InvalidPort(_) => None,
}
}
}
注意:
AppError::InvalidPort(_)
是业务错误,没有包装其他底层错误,所以返回:
None
配合 From 和 ?
为了让 ? 自动转换错误,需要实现 From:
impl From<io::Error> for AppError {
fn from(error: io::Error) -> Self {
AppError::Io(error)
}
}
impl From<ParseIntError> for AppError {
fn from(error: ParseIntError) -> Self {
AppError::Parse(error)
}
}
完整调用:
use std::fs;
fn load_port(filename: &str) -> Result<u16, AppError> {
let content = fs::read_to_string(filename)?;
let port = content.trim().parse::<u16>()?;
if port == 0 {
return Err(AppError::InvalidPort(port));
}
Ok(port)
}
这里:
fs::read_to_string(filename)?
原本返回:
std::io::Error
因为存在:
impl From<io::Error> for AppError
所以 ? 可以自动转换成:
AppError::Io(error)
同理:
content.trim().parse::<u16>()?
产生的 ParseIntError 会转换为:
AppError::Parse(error)
十一、Box<dyn Error> 是什么
经常可以看到:
fn run() -> Result<(), Box<dyn Error>> {
// ...
}
其中:
dyn Error
表示:
某一个实现了
Errortrait 的具体错误类型,但编译阶段不指定它到底是哪一种错误。
例如,它可能实际装着:
std::io::Error
也可能装着:
std::num::ParseIntError
或者:
ConfigError
由于 dyn Error 是动态大小类型,不能直接作为普通局部值使用,通常需要放在指针后面:
&dyn Error Box<dyn Error>
trait object 内部包含指向具体数据的指针以及用于动态方法调用的虚函数表。(Rust 文档)
使用示例
use std::error::Error;
use std::fs;
fn run() -> Result<(), Box<dyn Error>> {
let content = fs::read_to_string("config.txt")?;
let port = content.trim().parse::<u16>()?;
println!("端口号:{}", port);
Ok(())
}
fn main() -> Result<(), Box<dyn Error>> {
run()?;
Ok(())
}
这里可能产生:
std::io::Error
也可能产生:
std::num::ParseIntError
但是函数统一返回:
Box<dyn Error>
标准库为实现了 Error 的类型提供了到 Box<dyn Error> 的转换,因此很多不同错误都可以配合 ? 转换成动态错误。(Rust 文档)
十二、Box<dyn Error + Send + Sync> 是什么
在异步程序、多线程程序、Web 服务中,更常见的是:
Box<dyn Error + Send + Sync>
完整写法:
type BoxError = Box<dyn Error + Send + Sync + 'static>;
它表示这个错误:
- 实现了
Error - 可以在线程之间转移:
Send - 可以安全地在线程之间共享引用:
Sync - 不依赖短生命周期借用:
'static
例如:
use std::error::Error;
type BoxError = Box<dyn Error + Send + Sync + 'static>;
fn run() -> Result<(), BoxError> {
Ok(())
}
标准库也为满足 Error + Send + Sync 的错误提供了相应的 Box<dyn Error + Send + Sync> 转换。(Rust 文档)
十三、dyn Error 的向下转型
当错误被转换成:
Box<dyn Error>
之后,具体类型被隐藏了。
但可以使用:
is::<T>() downcast_ref::<T>() downcast_mut::<T>() downcast::<T>()
判断或恢复具体类型。
例如:
use std::error::Error;
use std::fmt;
#[derive(Debug)]
struct ConfigError {
filename: String,
}
impl fmt::Display for ConfigError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "配置文件错误")
}
}
impl Error for ConfigError {}
fn inspect_error(error: &(dyn Error + 'static)) {
if error.is::<ConfigError>() {
println!("这是 ConfigError");
}
if let Some(config_error) = error.downcast_ref::<ConfigError>() {
println!("配置文件:{}", config_error.filename);
}
}
官方 dyn Error 提供了 is()、downcast_ref() 等方法,用于判断和获取内部具体错误类型。(Rust 文档)
十四、'static 是什么意思
source() 的返回类型是:
Option<&(dyn Error + 'static)>
这里的 'static 约束的是:
dyn Error
表示底层错误类型不能包含非静态的借用数据。
例如,这种错误类型包含借用:
#[derive(Debug)]
struct MyError<'a> {
message: &'a str,
}
它不一定满足 'static,因为:
message
可能引用函数中的临时数据。
更常见、更容易作为错误源保存的写法是拥有数据:
#[derive(Debug)]
struct MyError {
message: String,
}
因为 String 自己拥有字符串内容,不依赖外部借用。
需要注意:
'static不代表这个错误对象一定存活到程序结束。
它主要表示该类型内部没有受限于较短生命周期的引用。
十五、Error 和 Result 的关系
Result 定义大致如下:
enum Result<T, E> {
Ok(T),
Err(E),
}
Result 并不要求:
E: Error
所以这些都合法:
Result<(), String>
Result<(), &'static str>
Result<(), u32>
Result<(), ConfigError>
但是只有实现了 Error 的类型,才能自然用于:
Box<dyn Error>
Error::source()
downcast_ref()
以及其他标准错误处理工具。
十六、错误类型一定要实现 Error 吗
不一定。
简单内部逻辑
可以直接使用:
Result<T, String>
例如:
fn validate_age(age: u8) -> Result<(), String> {
if age < 18 {
return Err("年龄必须大于或等于18岁".to_string());
}
Ok(())
}
公共库或大型项目
更推荐自定义错误类型并实现:
Debug Display Error
原因包括:
- 可以保存结构化字段
- 可以匹配错误类型
- 可以保存底层错误
- 可以建立错误链
- 可以使用
Box<dyn Error> - 更方便第三方库处理
例如,相比:
Err("配置文件读取失败".to_string())
下面更容易处理:
Err(AppError::ConfigRead {
path,
source,
})
十七、常见错误
1. 只实现 Error,没有实现 Display
错误代码:
#[derive(Debug)]
struct MyError;
impl Error for MyError {}
会因为没有实现 Display 而编译失败。
正确写法:
use std::error::Error;
use std::fmt;
#[derive(Debug)]
struct MyError;
impl fmt::Display for MyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "发生了错误")
}
}
impl Error for MyError {}
2. 忘记实现 Debug
错误代码:
struct MyError;
impl fmt::Display for MyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "发生了错误")
}
}
impl Error for MyError {}
通常直接添加:
#[derive(Debug)]
即可。
3. 有底层错误,却使用空实现
例如:
#[derive(Debug)]
struct ConfigError {
source: std::io::Error,
}
impl Error for ConfigError {}
这段代码可以编译,但错误链看不到底层的 io::Error。
应该实现:
impl Error for ConfigError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
Some(&self.source)
}
}
4. source() 返回错误自身
错误写法:
impl Error for MyError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
Some(self)
}
}
这样会产生无限循环错误链:
MyError
└── MyError
└── MyError
└── ...
source() 应该返回真正的底层错误,而不是当前错误自身。
5. 公共库只返回字符串
例如:
pub fn load_config() -> Result<(), String>
调用者只能查看字符串,难以判断:
- 是文件不存在
- 是权限不足
- 是 TOML 语法错误
- 是配置字段缺失
- 是字段值不合法
更合适的是定义枚举:
pub enum ConfigError {
FileNotFound,
PermissionDenied,
ParseError,
MissingField(String),
InvalidValue {
field: String,
value: String,
},
}
十八、std::error::Error、thiserror 和 anyhow
std::error::Error
标准库提供的基础能力,需要手动实现:
Debug Display Error From source
适合:
- 学习 Rust 错误机制
- 不希望增加第三方依赖
- 错误类型比较简单
thiserror
用于简化自定义错误类型的定义。
手动写法:
#[derive(Debug)]
enum AppError {
Io(std::io::Error),
}
使用 thiserror 后可以写成:
#[derive(Debug, thiserror::Error)]
enum AppError {
#[error("读取文件失败")]
Io(#[from] std::io::Error),
}
它会帮助生成:
DisplayErrorsource()- 部分
From
anyhow
主要用于应用程序中快速传播和包装错误:
fn run() -> anyhow::Result<()> {
// ...
Ok(())
}
通常可以理解为:
- 库代码:更倾向使用明确的自定义错误类型
- 应用代码:可以使用
anyhow - 自定义错误枚举:可以使用
thiserror
但它们的底层仍然建立在 Rust 标准错误 trait 体系之上。
十九、完整实战示例
下面综合使用:
- 自定义错误枚举
DisplayErrorsource()From?- 错误链输出
use std::error::Error;
use std::fmt;
use std::fs;
use std::io;
use std::num::ParseIntError;
#[derive(Debug)]
enum AppError {
ReadConfig {
path: String,
source: io::Error,
},
ParsePort {
value: String,
source: ParseIntError,
},
InvalidPort {
port: u16,
},
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AppError::ReadConfig { path, .. } => {
write!(f, "无法读取配置文件 `{}`", path)
}
AppError::ParsePort { value, .. } => {
write!(f, "无法将 `{}` 解析为端口号", value)
}
AppError::InvalidPort { port } => {
write!(f, "端口号 `{}` 无效", port)
}
}
}
}
impl Error for AppError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
AppError::ReadConfig { source, .. } => Some(source),
AppError::ParsePort { source, .. } => Some(source),
AppError::InvalidPort { .. } => None,
}
}
}
fn load_port(path: &str) -> Result<u16, AppError> {
let content = fs::read_to_string(path).map_err(|source| {
AppError::ReadConfig {
path: path.to_string(),
source,
}
})?;
let value = content.trim();
let port = value.parse::<u16>().map_err(|source| {
AppError::ParsePort {
value: value.to_string(),
source,
}
})?;
if port == 0 {
return Err(AppError::InvalidPort { port });
}
Ok(port)
}
fn print_error(error: &dyn Error) {
eprintln!("错误:{}", error);
let mut current = error.source();
while let Some(source) = current {
eprintln!("由此引起:{}", source);
current = source.source();
}
}
fn main() {
match load_port("config.txt") {
Ok(port) => {
println!("服务端口:{}", port);
}
Err(error) => {
print_error(&error);
}
}
}
假设文件不存在,输出类似:
错误:无法读取配置文件 `config.txt` 由此引起:系统找不到指定的文件
假设文件内容为:
abc
输出类似:
错误:无法将 `abc` 解析为端口号 由此引起:invalid digit found in string
二十、总结
std::error::Error 可以理解为 Rust 错误类型的统一标准接口。
一个标准自定义错误通常包含三部分:
#[derive(Debug)] struct MyError;
impl Display for MyError {
// 定义展示给用户的错误信息
}
impl Error for MyError {
// 可选:通过 source() 返回底层错误
}
最简单的错误:
#[derive(Debug)]
struct MyError;
impl Display for MyError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "发生错误")
}
}
impl Error for MyError {}
包装底层错误时:
impl Error for MyError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
Some(&self.source)
}
}
核心关系可以概括为:
Result<T, E>
│
└── E 可以是任意类型
│
└── 实现 Error 后
├── 必须实现 Debug
├── 必须实现 Display
├── 可以提供 source()
├── 可以形成错误链
├── 可以转换为 Box<dyn Error>
└── 可以进行动态类型判断和向下转型
推荐博客标题:
《Rust std::error::Error 详解:自定义错误、错误链与 Box