Rust 2026 经验谈 - 错误处理体系 2026

2613 字
13 分钟
Rust 2026 经验谈 - 错误处理体系 2026

错误处理是 Rust 的核心设计优势之一——Result<T, E>? 操作符的组合让错误传播既安全又优雅。但”用什么类型做 E”这个问题的答案在 2026 年已经丰富到需要一篇专题来梳理。本文将从库/应用分界的架构视角出发,深入 thiserror 2.0 + anyhow 1.0 的最佳实践、Error source chain 的遍历、? 操作符的推导细节、Provider API 的前沿进展,以及 eyre vs anyhow 的选型。

thiserror 2.0 + anyhow 1.0:架构分工#

Rust 错误处理的黄金法则是:库用 thiserror,应用用 anyhow。这不是教条,而是有深层原因的架构决策。

库的错误类型:thiserror 2.0#

库的 Error 类型是公共 API 的一部分。调用者需要:

  1. 匹配特定错误match err { MyError::NotFound => ... }
  2. 提取错误详情if let MyError::Timeout { duration } = err { ... }
  3. 错误链追踪:从底层错误向上传播

thiserror 2.0 通过 derive 宏自动实现 std::error::ErrorDisplayFrom

use thiserror::Error;
#[derive(Error, Debug)]
pub enum DataStoreError {
#[error("data store disconnected")]
Disconnect(#[from] io::Error),
#[error("the data for key `{0}` is not available")]
Redaction(String),
#[error("invalid header (expected {expected:?}, found {found:?})")]
InvalidHeader {
expected: String,
found: String,
},
#[error("unknown data store error")]
Unknown,
}

thiserror 2.0 的新特性

use thiserror::Error;
#[derive(Error, Debug)]
pub enum AppError {
// #[error(transparent)] 透传底层错误(1.0 已支持,2.0 改进与 Provider API 协作)
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
Parse(#[from] std::num::ParseIntError),
// 2.0 改进的 #[source] 与 Display 格式化协作
#[error("configuration error at {path}:{line}")]
Config {
path: String,
line: usize,
#[source]
source: ConfigParseError,
},
}
#[derive(Error, Debug)]
#[error("invalid config value: {msg}")]
struct ConfigParseError {
msg: String,
}

应用的错误类型:anyhow 1.0#

应用的错误处理不需要匹配具体类型——你只需要:

  1. 记录/展示错误(附带上下文)
  2. 尽早退出? 传播)
  3. 偶尔 downcast 检查特定错误

anyhow 1.0 提供这一切:

use anyhow::{Context, Result, anyhow, ensure};
fn read_config(path: &str) -> Result<Config> {
let content = std::fs::read_to_string(path)
.with_context(|| format!("failed to read config from {}", path))?;
let config: Config = toml::from_str(&content)
.context("failed to parse config")?;
ensure!(config.port > 0, "port must be positive, got {}", config.port);
Ok(config)
}

with_context 的威力:它在错误链上附加上下文信息,让”配置文件不存在”这样的底层错误变成”failed to read config from /etc/app.toml: No such file or directory”——带有完整的因果链。

为什么不能混用#

// ❌ 库返回 anyhow::Error
pub fn parse(data: &[u8]) -> anyhow::Result<Parsed> {
// ...
}
// 问题:调用者无法匹配具体错误
fn caller() {
match parse(&data) {
Ok(parsed) => { /* ... */ },
Err(e) => {
// e 是 anyhow::Error,只能 downcast
if e.is::<std::io::Error>() {
// 但这很脆弱,且丧失了穷尽性检查
}
},
}
}
// ✅ 库返回具体错误类型
pub fn parse(data: &[u8]) -> Result<Parsed, ParseError> {
// ...
}
fn caller() {
match parse(&data) {
Ok(parsed) => { /* ... */ },
Err(ParseError::InvalidFormat(line)) => { /* 处理格式错误 */ },
Err(ParseError::Overflow) => { /* 处理溢出 */ },
}
}

分工原则

  • 库(library crate):定义具体 Error 枚举(thiserror),让调用者可以模式匹配
  • 应用(binary crate):用 anyhow::Result 收集所有错误,附加上下文,记录日志
  • 库的内部:可以用 anyhow 做快速原型,但公共 API 必须用具体类型

Error source chain:遍历因果链#

std::error::Error::source() 方法返回错误的原因(cause),形成一条链。这是错误报告和日志系统的核心。

手动遍历 source chain#

use std::error::Error;
fn print_error_chain(err: &dyn Error) {
let mut level = 0;
let mut source = Some(err);
while let Some(e) = source {
println!("{:width$}→ {}", level, e, width = level * 2);
source = e.source();
level += 1;
}
}
// 输出示例:
// → failed to read config from /etc/app.toml
// → No such file or directory (os error 2)

thiserror 的 #[from] 自动实现 source#

#[derive(Error, Debug)]
pub enum StoreError {
#[error("database query failed")]
Query(#[from] sql::Error), // #[from] 自动实现 From<sql::Error> 和 source()
#[error("connection pool exhausted")]
PoolExhausted,
}
// sql::Error 作为 StoreError::Query 的 source
fn demo(err: StoreError) {
if let Some(source) = err.source() {
println!("caused by: {}", source); // 打印 sql::Error
}
}

anyhow 的错误链#

anyhow 的 context()with_context() 在错误链上插入上下文节点:

use anyhow::{Context, Result};
fn deep_operation() -> Result<()> {
std::fs::read_to_string("missing.txt")
.context("reading config")?; // 错误链:reading config → No such file or directory
Ok(())
}

anyhow 的 Chain 迭代器

fn print_anyhow_chain(err: &anyhow::Error) {
for (i, cause) in err.chain().enumerate() {
println!("{:2}: {}", i, cause);
}
}

? 操作符类型推导细节#

? 操作符的行为比大多数人理解的更微妙。它不仅传播错误,还执行 From 转换。

核心机制#

fn foo() -> Result<i32, AppError> {
let n: i32 = "42".parse()?; // parse 返回 Result<i32, ParseIntError>
// ? 做了两件事:
// 1. 如果 Err(e),调用 From::<ParseIntError>::from(e) 转为 AppError
// 2. 提前返回 Err(AppError::...)
Ok(n)
}

推导步骤

  1. 编译器知道函数返回 Result<_, AppError>
  2. ? 遇到 Result<_, ParseIntError>Err 分支
  3. 查找 From<ParseIntError> for AppError 的实现
  4. 如果找到,调用 from() 转换;如果没找到,编译错误

From vs FromIterator 的坑#

fn process(items: &[&str]) -> Result<Vec<i32>, AppError> {
// ❌ 常见错误:试图对 Iterator<Item=Result<_, _>> 使用 ?
// items.iter().map(|s| s.parse::<i32>()).collect()?
// ^^^^^^^^ 返回 Result<i32, ParseIntError>
// collect() 对 Iterator<Item=Result<i32, ParseIntError>> 的行为是:
// 如果任何元素是 Err,返回 Err(第一个错误)
// 否则返回 Ok(Vec<i32>)
// 但 ? 作用于 collect 的结果,类型是 Result<Vec<i32>, ParseIntError>
// 需要 From<ParseIntError> for AppError
// ✅ 正确写法
items.iter()
.map(|s| s.parse::<i32>().map_err(AppError::from))
.collect::<Result<Vec<_>, _>>()
}

关键区别

  • collect::<Result<Vec<T>, E>>() 利用 FromIterator for Result——任何一个元素失败则整体失败
  • ? 利用 From<E1> for E2——把一种错误类型转为另一种

混用时,? 作用于 collect() 的结果(Result<Vec<T>, E1>),不是作用于每个元素。

? 在 Option 中的行为#

fn find_and_process(data: &[i32]) -> Option<i32> {
let first = data.first()?; // ? 在 Option 上下文中:None 则提前返回
Some(*first * 2)
}

?Option 上下文中传播 None,不做 From 转换。注意ResultOption 之间的 ? 不能混用——Result 中的 ? 不能传播到返回 Option 的函数,反之亦然(除非有 From 实现)。

fn mixed() -> Option<i32> {
// ❌ "42".parse::<i32>().ok()? 可以工作,但这是手动转换
// 直接用 ? 不行:Result 和 Option 的 ? 不互通
"42".parse::<i32>().ok()? // ✅ ok() 将 Result 转为 Option
}

Provider API:替代 Error::source 获取上下文#

Provider API(std::any::Provider / std::any::Demand)是 Rust 1.86 引入的类型按需提供机制,比 Error::source() 更灵活。但作为错误上下文提取的完整 API,其稳定化仍在推进中。

问题:source() 只返回一个原因#

Error::source() 只能返回一个源错误。但有时候错误有多个相关信息:

#[derive(Error, Debug)]
#[error("request to {url} failed with status {status}")]
struct RequestError {
url: String,
status: u16,
source: std::io::Error,
}
// source() 只返回 std::io::Error
// url 和 status 丢失了——它们不在 source chain 中

传统方案是把所有信息塞进 Display 消息中,但这不可编程——你只能得到字符串,无法提取结构化数据。

Provider API 的解决方案#

Provider API 允许错误按类型提供上下文值:

use std::any::{Demand, Provider};
impl Provider for RequestError {
fn provide<'a>(&'a self, demand: &mut Demand<'a>) {
demand.provide_ref::<String>(&self.url);
demand.provide_ref::<u16>(&self.status);
demand.provide_ref::<std::io::Error>(&self.source);
}
}
// 消费者按类型提取上下文
fn inspect(err: &dyn Provider) {
if let Some(url) = demand.provide_ref::<String>() {
println!("failed URL: {}", url);
}
// 注意:此处展示的是 Provider API 的核心思路
// 完整的 demand 提取 API 在稳定化过程中可能有所调整
}

vs Error::source()

维度source()Provider API
返回值数量1 个任意多个
类型安全&dyn Error按请求类型返回
向后兼容是(Provider 有默认空实现)
提取方式遍历链按类型 demand

稳定化状态:Provider API 的核心 trait(Provider/Demand)已在 Rust 1.86 稳定,但用于错误上下文提取的便捷 API(如 demand_ref 等函数级接口)仍在完善中。anyhoweyre 已部分采用 Provider 思路,等完整 API 稳定后会逐步迁移。

thiserror 2.0 对 Provider 的支持#

thiserror 2.0 已经为 Provider API 做了准备:#[source] 标注的字段会同时实现 provide 方法,让你可以通过 Provider API 提取 source 而不仅靠 source() 方法。

eyre vs anyhow 选型#

eyre 是 anyhow 的 fork,核心差异在于错误报告(error report)的自定义能力

anyhow 的报告格式#

Error: failed to read config from /etc/app.toml
Caused by:
No such file or directory (os error 2)

eyre 的报告格式#

Error:
0: failed to read config from /etc/app.toml
1: No such file or directory (os error 2)
Location:
src/main.rs:15:5

eyre 的报告包含源码位置(文件名、行号、列号),这在调试时非常有用。

核心对比#

维度anyhoweyre
错误报告简洁,无位置信息详细,含位置信息
自定义报告不可可(通过 Handler trait)
性能稍快(不捕获位置)稍慢(捕获位置 + backtrace)
依赖仅 thiserror(可选)color-eyre(推荐)
生态成熟度更成熟,更多文档活跃但较小

选型建议#

// 场景 1:库的内部 / 简单应用
use anyhow::Result; // anyhow 足够:轻量、快速
// 场景 2:需要详细错误报告的应用(CLI、服务端)
use eyre::Result; // eyre + color-eyre:彩色报告 + backtrace + 位置
// 场景 3:嵌入式 / 极端性能敏感
// 手动定义 Error 枚举,不用 anyhow/eyre,避免分配

color-eyre 的配置#

use color_eyre::{eyre::eyre, Help, Result};
fn read_config() -> Result<Config> {
let content = std::fs::read_to_string("config.toml")
.with_suggestion(|| "try creating config.toml with default settings")?; // 帮助建议
Ok(toml::from_str(&content)?)
}
fn main() -> Result<()> {
color_eyre::install()?; // 初始化 color-eyre 的错误处理器
let config = read_config()?;
Ok(())
}

with_suggestion 是 color-eyre 的特色功能——在错误报告中附加可操作的建议,这在 CLI 工具中特别有用。

实战:完整的错误处理架构#

以一个 HTTP 服务为例,展示 thiserror + anyhow 的分层架构:

// ===== 库:db crate =====
use thiserror::Error;
#[derive(Error, Debug)]
pub enum DbError {
#[error("connection failed to {host}:{port}")]
ConnectionFailed {
host: String,
port: u16,
#[source]
source: std::io::Error,
},
#[error("query execution failed")]
Query(#[from] sql::Error),
#[error("record not found: {table}/{id}")]
NotFound { table: String, id: i64 },
#[error("pool exhausted, max: {max}")]
PoolExhausted { max: usize },
}
// ===== 库:api crate =====
#[derive(Error, Debug)]
pub enum ApiError {
#[error("database error")]
Db(#[from] DbError),
#[error("invalid request: {0}")]
Validation(String),
#[error("unauthorized")]
Unauthorized,
#[error("rate limited, retry after {retry_after_secs}s")]
RateLimited { retry_after_secs: u64 },
}
// ===== 应用:server binary =====
use anyhow::{Context, Result};
fn handle_request(req: Request) -> Result<Response> {
let user = authenticate(&req)
.context("authentication failed")?;
let data = query_database(&user)
.with_context(|| format!("query failed for user {}", user.id))?;
Ok(Response::ok(data))
}

关键设计

  • DbErrorApiError 是具体类型,库的用户可以模式匹配
  • 应用层用 anyhow::Result 收集,context() 附加业务上下文
  • #[from]? 自动转换:DbErrorApiError::Db
  • 错误链:anyhow contextApiErrorDbErrorstd::io::Error

小结#

Rust 的错误处理体系在 2026 年已经形成清晰的最佳实践:

  • 库/应用分界:库用 thiserror 定义具体 Error 枚举,应用用 anyhow/eyre 收集并附加上下文
  • Error source chainsource() 方法形成因果链,是错误报告的核心
  • ? 操作符:通过 From 自动转换错误类型,理解其推导机制才能写出正确的错误转换链
  • Provider APIprovide/demand 机制比 source() 更灵活,允许错误按类型提供多个上下文值
  • eyre vs anyhow:简单场景用 anyhow,需要详细报告(位置、backtrace、建议)用 eyre + color-eyre

最核心的一条原则:错误类型是你的 API 的一部分。对库来说,错误枚举的设计和函数签名的设计同等重要——它决定了调用者能多精细地处理异常情况。对应用来说,错误信息的质量直接决定了运维排障的效率。

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!

赞助
Rust 2026 经验谈 - 错误处理体系 2026
https://tinyzzh.github.io/posts/rust-2026/2026-06-10-rust_2026_010_error_handling_2026/
作者
TinyZ Zzh
发布于
2026-06-10
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
TinyZ Zzh
专注于高并发服务器、网络游戏相关(Java、PHP、Unity3D、Unreal Engine等)技术,热爱游戏事业, 正在努力实现自我价值当中。
公告
欢迎来到我的博客!这是一则示例公告。
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
标签
站点统计
文章
224
分类
38
标签
237
总字数
369,260
运行时长
0
最后活动
0 天前

文章目录