不灭的焱

革命尚未成功,同志仍须努力 下载Java21

作者:AlbertWen  添加时间:2026-07-20 09:55:14  修改时间:2026-07-25 15:02:35  分类:01.Rust编程  编辑

目录

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 定义为错误值应满足的基本规范,并要求实现者同时实现 DebugDisplay。(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() 已废弃,应该使用 Displayto_string()
  • cause() 已废弃,应该使用 source()
  • provide() 目前仍是 nightly 实验性功能
  • source() 用于返回底层错误原因

(Rust 文档)

二、Error: Debug + Display 是什么意思

下面这段定义:

pub trait Error: Debug + Display

表示:

任何实现了 Error 的类型,都必须同时实现 DebugDisplay

也就是说,下面的代码不能直接编译:

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 {}

三、DebugDisplayError 分别负责什么

可以简单理解为:

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() 用于返回:

导致当前错误的底层错误。

例如:

  1. 程序读取配置文件
  2. 配置文件读取成功
  3. 但是端口字段不是数字
  4. 底层产生 ParseIntError
  5. 上层将其包装为 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

九、不要在 Displaysource() 中重复输出底层错误

假设当前错误包装了底层错误。

不推荐这样写:

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

表示:

某一个实现了 Error trait 的具体错误类型,但编译阶段不指定它到底是哪一种错误。

例如,它可能实际装着:

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 不代表这个错误对象一定存活到程序结束。

它主要表示该类型内部没有受限于较短生命周期的引用。

十五、ErrorResult 的关系

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::Errorthiserroranyhow

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),
}

它会帮助生成:

  • Display
  • Error
  • source()
  • 部分 From

anyhow

主要用于应用程序中快速传播和包装错误:

fn run() -> anyhow::Result<()> {
    // ...
    Ok(())
}

通常可以理解为:

  • 库代码:更倾向使用明确的自定义错误类型
  • 应用代码:可以使用 anyhow
  • 自定义错误枚举:可以使用 thiserror

但它们的底层仍然建立在 Rust 标准错误 trait 体系之上。

十九、完整实战示例

下面综合使用:

  • 自定义错误枚举
  • Display
  • Error
  • source()
  • 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