引言
这是Rust九九八十一难第十四篇,介绍下thiserror组件。之前聊过anyhow,提到thiserror+anyhow才是最佳组合,这次把缺少的内容补上。目前Rust工程界普遍引入了thiserror,原因是存在一些痛点,比如手写 Display 太麻烦,业务逻辑开没敲,就要堆样板代码,类似下面这种。
rust
use std::fmt;
#[derive(Debug)]
pub enum MyError {
Config(String),
Io(std::io::Error),
}
impl fmt::Display for MyError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
MyError::Config(s) => write!(f, "Config error: {}", s),
MyError::Io(e) => write!(f, "IO error: {}", e),
}
}
}
impl std::error::Error for MyError {}
或者写大量错误间的 From 转换:
rust
impl From<std::io::Error> for MyError {
fn from(e: std::io::Error) -> Self {
MyError::Io(e)
}
}
thiserror可以较好的解决这些问题,下面先入个门。
一、简单入门
跟之前一样,先发个入门示例,复制粘贴就可以运行,方便理解。
-
项目结构
cssthiserror-demo/ ├── Cargo.toml └── src └── main.rs -
Cargo.toml, 添加依赖,需要 rustc 1.68 及以上
yaml[package] name = "thiserror-demo" version = "0.1.0" edition = "2021" [dependencies] thiserror = "2" anyhow = "1" -
rust
use std::fs::File; use std::io::{self, Read}; use thiserror::Error; /// 自定义错误类型 #[derive(Debug, Error)] enum AppError { /// IO 错误自动转换 #[error("IO 错误: {0}")] Io(#[from] io::Error), /// 自定义错误 #[error("配置格式错误: {0}")] InvalidConfig(String), /// anyhow 用于兜底错误 #[error("内部错误: {0}")] Internal(#[from] anyhow::Error), } /// 读取配置文件 fn read_config(path: &str) -> Result<String, AppError> { let mut f = File::open(path)?; // 自动转成 AppError::Io let mut s = String::new(); f.read_to_string(&mut s)?; if !s.starts_with("version") { return Err(AppError::InvalidConfig("缺少 version 字段".into())); } Ok(s) } fn main() -> Result<(), AppError> { match read_config("config.txt") { Ok(content) => println!("读取成功:\n{content}"), Err(err) => println!("发生错误: {err}"), } Ok(()) } -
cargo run
输出:发生错误: IO 错误: No such file or directory (os error 2)
二、核心API介绍
1、#[derive(Error)]+#[error(...)]
- 给 enum 或 struct 派生
std::error::Error - 定义 Display 输出,渲染占位符
rust
use thiserror::Error;
#[derive(Error, Debug)]
pub enum MyError {
#[error("IO 错误: {0}")] //元组结构或 enum 的第 0 个字段display拼接到IO错误里
Io(std::io::Error),
#[error("自定义错误: {msg}")] //显示 msg 的内容
Custom { msg: String },
}
fn demo_io_error() -> Result<(), MyError> {
let _ = std::fs::read_to_string("不存在的文件.txt")
.map_err(MyError::Io)?;
Ok(())
}
fn demo_custom_error() -> Result<(), MyError> {
Err(MyError::Custom { msg: "非法状态".into() })
}
fn main() {
println!("{}", demo_io_error().unwrap_err());//调用display
println!("{:?}", demo_io_error().unwrap_err());//调用Debug
println!("{:?}", demo_custom_error());//调用Debug
println!("{}", demo_custom_error().unwrap_err());//调用display
}
输出结果:
css
IO 错误: No such file or directory (os error 2)
Io(Os { code: 2, kind: NotFound, message: "No such file or directory" })
Err(Custom { msg: "非法状态" })
自定义错误: 非法状态
说明:
-
注意区分debug和display,thiserror渲染的是display内容
-
强烈建议Debug宏也加上,方便调试,与anyhow配合打印错误链
-
{0} 引用 元组结构或 enum 变体的第 0 个字段,也可以多个和使用字段名,可以拼接,比如下面这种
rust#[derive(Debug, Error)] pub enum StructError { #[error("用户错误: {id} - {msg}")] User { id: i32, msg: String }, }它调用的是字段 Display trait方法,比如std::io::Error的display输出
2、自动转换From :#[from]
- 该操作自动把下层错误类型转成thiserror 类型
- 支持
?链式调用
rust
use thiserror::Error;
#[derive(Error, Debug)]
pub enum AppError {
#[error("解析失败: {0}")]
Parse(#[from] std::num::ParseIntError),
}
fn demo_from() -> Result<i32, AppError> {
let s = "abc";
let n: i32 = s.parse()?; // parse 返回 ParseIntError,自动转AppError
Ok(n)
}
fn main() {
println!("{}", demo_from().unwrap_err());
//解析失败: invalid digit found in string
}
说明:
-
#[from] 自动实现了From for AppError:
rustimpl From<ParseIntError> for AppError { fn from(err: ParseIntError) -> Self { AppError::Parse(err) } } -
parse 返回 ParseIntError后走到AppError::Parse(err),实现链式调用。thiserror 的 #[from] 会自动生成
Fromimpl,无需手动写。
3、返回底层错误:#[source]
3.1、基本使用
一个小背景:std::error::Error trait 有一个方法 fn source(&self) -> Option<&(dyn Error + 'static)>它用于返回 底层引起当前错误的原始错误,可以构建 错误链(error chain),方便调试和日志打印。
#[source] 显式标记一个字段是"底层错误",生成的 Error::source() 会返回这个字段。
rust
#[derive(Error, Debug)]
pub enum DbError {
#[error("查询失败: {0}")]
QueryFailed(#[source] std::io::Error),
}
fn demo_source() -> Result<(), DbError> {
let _ = std::fs::read_to_string("不存在的文件.txt")
.map_err(DbError::QueryFailed)?;
Ok(())
}
fn main() {
let err = demo_source().unwrap_err();
println!("{}", err); // 查询失败: No such file or directory (os error 2)
println!("{:?}", err.source()); // Some(Os { code: 2, kind: NotFound, message: "No such file or directory" })
}
3.2、理解错误链
rust
use thiserror::Error;
use std::io;
#[derive(Debug, Error)]
pub enum DbError {
#[error("数据库查询失败: {query}")]
Query {
query: String,
#[source]
source: io::Error,
},
}
#[derive(Debug, Error)]
pub enum AppError {
#[error("业务处理失败: {0}")]
Db(#[from] DbError),
#[error("解析失败: {0}")]
Parse(#[from] std::num::ParseIntError),
}
fn db_layer() -> Result<(), DbError> {
let io_err = io::Error::new(io::ErrorKind::NotFound, "文件不存在");
Err(DbError::Query { query: "SELECT *".into(), source: io_err })
}
fn app_layer() -> Result<(), AppError> {
// 底层 db_layer 错误通过 #[from] 自动转成 AppError::Db
db_layer()?;
Ok(())
}
fn main() {
let err = app_layer().unwrap_err();
println!("Display: {}", err);
// 遍历多层 source
let mut source = err.source();
let mut level = 1;
while let Some(s) = source {
println!("Source level {}: {} ({:?})", level, s, s);
source = s.source();
level += 1;
}
}
输出结果:
less
Display: 业务处理失败: 数据库查询失败: SELECT *
Source level 1: 数据库查询失败: SELECT * (Query { query: "SELECT *", source: Custom { kind: NotFound, error: "文件不存在" } })
Source level 2: 文件不存在 (Custom { kind: NotFound, error: "文件不存在" })
说明:
#[source] 标记底层错误 ,配合Error::source()访问层级错误
-
a、最顶层:AppError::Db
-
Display :"业务处理失败: 数据库查询失败: SELECT *"
-
source() : DbError::Query
-
-
b、中间层:DbError::Query
-
Display : "数据库查询失败: SELECT *"
-
source() : io::Error
-
-
c、底层:io::Error
-
Display → "文件不存在"
-
source() → None
-
3.2、 #from和#source一起用
rust
use std::error::Error;
use std::io;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum DbError {
#[error("数据库查询失败: {0}")]
Query(#[from]#[source] io::Error),
}
fn db_layer() -> Result<(), DbError> {
let io_err = io::Error::new(io::ErrorKind::NotFound, "文件不存在");
Err(io_err.into()) // #[from] 支持自动 From
}
fn main() {
let err = db_layer().unwrap_err();
println!("Display: {}", err); // Display: 数据库查询失败: 文件不存在
println!("Source: {:?}", err.source()); // Source: Some(Custom { kind: NotFound, error: "文件不存在" })
}
- 字段类型必须实现 std::error::Error,才能作为 source
- 库层枚举(io::Error、sqlx::Error 等)字段常用
#[from]#[source],方便追踪;应用层 enum 可以只用#[from]或只用#[source]
4、#[error(transparent)]
-
封装底层error,透明传递底层错误,不想额外加前缀或者信息.
-
常用于库层错误封装 +
#[from]自动转换
rust
use thiserror::Error;
use std::io;
#[derive(Debug, Error)]
pub enum MyError {
#[error(transparent)]
Io(#[from] io::Error),
}
/// 自定义错误类型
fn read_file() -> Result<String, MyError> {
let content = std::fs::read_to_string("不存在.txt")?; // io::Error -> MyError::Io
Ok(content)
}
fn main() {
let err = read_file().unwrap_err();
println!("Display: {}", err); // 直接显示底层 io::Error 的信息:Display: No such file or directory (os error 2)
println!("Source: {:?}", err); // 返回 io::Error:Source: Io(Os { code: 2, kind: NotFound, message: "No such file or directory" })
}
注意:
-
只支持单个字段
rust#[error(transparent)] Io(#[from] io::Error); // 能编译通过 Io { source: io::Error, code: i32 } // 编译失败,不支持多个字段
5、backtrace
捕获构造错误时的错误堆栈。thiserror 自动调用 Backtrace::capture()存进去。
rust
#[derive(thiserror::Error, Debug)]
pub enum MyError {
#[error("IO 失败")]
Io {
#[from]
source: std::io::Error,
#[backtrace]
backtrace: std::backtrace::Backtrace,
}
}
注意:
- Rust 在默认情况下禁用 backtrace(为了性能)
- 要求使用 nightly 编译器,且 Rust 版本为 1.73 或更高
三、一些高级技巧
1、用 #[error(transparent)] 实现 "error wrapper"
rust
#[derive(Error, Debug)]
pub enum AppError {
#[error(transparent)]
Db(#[from] sqlx::Error),
#[error(transparent)]
Anyhow(#[from] anyhow::Error),
#[error("业务错误: {0}")]
Biz(String),
}
-
统一错误出口
-
SQLx/Reqwest/IO/Anyhow 的错误自动转型
-
最后 AppError 做 API/Trace 转换
2、结合 #[source] 与上下文(Context)字段
结构化处理错误,解决只有底层报错但缺乏业务上下文(如文件名、行号、请求ID)的问题。
rust
use thiserror::Error;
use std::path::PathBuf;
use std::fs::File;
use std::io;
#[derive(Error, Debug)]
pub enum ConfigError {
// 注意:这里不能用#[from],因为需要额外的 path 字段
// #[source]告诉thiserror这个字段是底层错误源
#[error("读取配置文件 '{path}' 失败")]
ReadFailed {
path: PathBuf,
#[source]
source: io::Error,
},
}
fn load_config(path: PathBuf) -> Result<(), ConfigError> {
// 手动map_err注入path上下文
File::open(&path).map_err(|e| ConfigError::ReadFailed {
path: path.clone(),
source: e,
})?;
Ok(())
}
fn main() {
let path = PathBuf::from("missing_config.toml");
if let Err(e) = load_config(path) {
println!("{}", e); // 输出: 读取配置文件 'missing_config.toml' 失败
// 验证 source 链
use std::error::Error;
if let Some(src) = e.source() {
println!("Caused by: {}", src); // 输出: Caused by: No such file or directory
}
}
}
3、处理泛型错误(Generic Errors)
错误类型需要包含用户自定义的数据类型,或者错误的具体类型由泛型决定。
rust
use thiserror::Error;
use std::fmt::Debug;
// T 必须满足 Debug 和 Display,以便能被 #[error] 宏使用
#[derive(Error, Debug)]
pub enum ParseError<T: Debug + std::fmt::Display> {
#[error("在位置 {location} 发现非法 Token: {token}")]
InvalidToken {
token: T,
location: usize,
},
#[error("未知的处理错误")]
Unknown,
}
fn main() {
let err = ParseError::InvalidToken {
token: "EOF", // 这里 T 是 &str
location: 42,
};
println!("{}", err);
}
4、枚举分层拆分(模块化错误)
可以这样定义
rust
errors/
├─ mod.rs
├─ io_error.rs
├─ service_error.rs
└─ db_error.rs
然后统一 wrap:
csharp
#[derive(Error, Debug)]
pub enum AppError {
#[error(transparent)]
Io(#[from] IoError),
#[error(transparent)]
Db(#[from] DbError),
}
5、与anyhow怎么分界
简单说thiserror负责"产出"错误,anyhow 负责"消费"错误。
-
库代码用thiserror,比如db,cache
-
业务层优先 thiserror,复杂链路可结合,错误穿透到 main用anyhow
-
数据库、HTTP、序列化:thiserror
-
入口层(Controller / handler):anyhow + Context
-
-
在 Web 服务中,通常 Controller/Handler 层是分界线。
- Service 层返回
Result<T, AppError>(用thiserror)。 - Handler 捕获
AppError,将其转换为 HTTP Response。 - 如果 Service 层遇到意料之外的 bug(比如 panic 或不可恢复的 IO 错),才会被
anyhow捕获并记录为 500 错误。
- Service 层返回
rust
use thiserror::Error;
use anyhow::{Context, Result};
// ==========================================
// 1. 边界下层:基础设施 / 库 (thiserror 领域)
// ==========================================
#[derive(Error, Debug)]
pub enum DbError {
#[error("连接数据库失败")]
ConnectionFailed,
#[error("查询语法错误")]
QueryError,
}
// 函数签名:只可能犯这两种错
fn query_user_from_db(id: u32) -> std::result::Result<String, DbError> {
if id == 0 {
return Err(DbError::ConnectionFailed);
}
Ok("Alice".to_string())
}
// ==========================================
// 2. 边界上层:业务逻辑 / 应用 (anyhow 领域)
// ==========================================
fn handle_request(user_id: u32) -> Result<String> {
// 🔥 分界点在这里! 🔥
// 我们调用了返回具体错误的底层函数,
// 但我们立刻用 context() 把它转换成了 anyhow::Error。
// 从这一行开始,具体的 DbError 类型信息被"擦除"了,
// 变成了一个通用的错误对象。
let user = query_user_from_db(user_id)
.context(format!("处理用户请求 ID:{} 失败", user_id))?;
Ok(user)
}
fn main() {
if let Err(e) = handle_request(0) {
eprintln!("Error: {:?}", e);
}
}
输出结果:
c
Error: 处理用户请求 ID:0 失败
Caused by:
连接数据库失败
Stack backtrace:
0: std::backtrace_rs::backtrace::libunwind::trace
at /rustc/ed61e7d7e242494fb7057f2657300d9e77bb4fcb/library/std/src/../../backtrace/src/backtrace/libunwind.rs:117:9
既有适合返回的Error,也有适合查询错的casued by
四、总结
本文介绍了thiserror的基本使用和高级技巧。从axum和thiserror官方看,推荐anyhow+thiserror组合。但是这俩也有缺点,比如错误层级多匹配困难,还需结合error-stack等库一起用。
如果觉得本文有用,请点个关注吧,本人公众号大鱼七成饱。