目录
- Rust thiserror 详细使用教程
- 一、thiserror 解决什么问题
- 二、安装 thiserror
- 三、#[error(...)] 的基本用法
- 四、在函数中返回 thiserror 错误
- 五、#[from]:自动转换底层错误
- 六、#[source]:建立错误来源链
- 七、#[error(transparent)]:透明包装错误
- 八、结构体错误类型
- 九、完整案例:读取应用配置
- 十、根据错误变体执行不同逻辑
- 十一、为错误增加业务上下文
- 十二、模块化项目中的错误设计
- 十三、Web 项目中的使用方式
- 十四、thiserror 和 anyhow 的区别
- 十五、多个相同底层错误时的处理
- 十六、保存额外上下文数据
- 十七、泛型错误类型
- 十八、SeaORM 项目示例
- 十九、Redis 错误示例
- 二十、错误测试
- 二十一、#[backtrace]
- 二十二、错误可见性设计
- 二十三、常见错误与注意事项
- 二十四、推荐的错误分层方案
- 二十五、完整可运行综合示例
- 二十六、核心属性速查表
- 二十七、使用建议总结
Rust thiserror 详细使用教程
thiserror 是 Rust 生态中用于定义结构化自定义错误类型的常用库。它通过 #[derive(Error)] 自动生成:
std::error::Error实现Display实现- 部分
From<T>实现 - 错误来源链
source()
截至 2026 年 7 月,crates.io 上的当前版本是 thiserror 2.0.18。官方提供的派生属性主要包括 #[error]、#[from]、#[source] 和 #[backtrace]。(crates.io)
一、thiserror 解决什么问题
假设我们不使用 thiserror,要定义一个自定义错误:
use std::fmt;
#[derive(Debug)]
pub enum AppError {
NotFound(String),
InvalidInput(String),
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AppError::NotFound(name) => {
write!(f, "资源不存在:{name}")
}
AppError::InvalidInput(message) => {
write!(f, "输入参数错误:{message}")
}
}
}
}
impl std::error::Error for AppError {}
代码能用,但每定义一个错误枚举,都要手动实现:
Display Error From source
使用 thiserror 后可以简化为:
use thiserror::Error;
#[derive(Debug, Error)]
pub enum AppError {
#[error("资源不存在:{0}")]
NotFound(String),
#[error("输入参数错误:{0}")]
InvalidInput(String),
}
thiserror 本质上是帮你生成标准 Rust 错误 trait 的实现;它不会把特殊的 thiserror 类型暴露在你的公开 API 中。因此,将手写实现改成 thiserror,通常不会改变调用方看到的错误接口。(Docs.rs)
二、安装 thiserror
1. 使用 cargo 命令
cargo add thiserror
会在 Cargo.toml 中增加类似配置:
[dependencies] thiserror = "2"
也可以指定当前具体版本:
[dependencies] thiserror = "2.0.18"
通常推荐写主版本:
thiserror = "2"
这样允许 Cargo 自动采用兼容的 2.x 新版本。
2. 最小项目示例
项目结构:
thiserror-demo/
├── Cargo.toml
└── src/
└── main.rs
Cargo.toml:
[package] name = "thiserror-demo" version = "0.1.0" edition = "2024" [dependencies] thiserror = "2"
src/main.rs:
use thiserror::Error;
#[derive(Debug, Error)]
enum AppError {
#[error("用户不存在:{0}")]
UserNotFound(u64),
#[error("用户名不能为空")]
EmptyUsername,
}
fn find_user(user_id: u64) -> Result<String, AppError> {
if user_id == 1 {
Ok("Albert".to_string())
} else {
Err(AppError::UserNotFound(user_id))
}
}
fn main() {
match find_user(100) {
Ok(username) => {
println!("找到用户:{username}");
}
Err(error) => {
println!("查询失败:{error}");
println!("调试信息:{error:?}");
}
}
}
运行:
cargo run
输出:
查询失败:用户不存在:100 调试信息:UserNotFound(100)
这里:
println!("{error}");
调用的是自动生成的 Display。
而:
println!("{error:?}");
调用的是 Debug。
因此,错误类型一般同时派生:
#[derive(Debug, Error)]
三、#[error(...)] 的基本用法
#[error(...)] 用于定义错误显示文本,thiserror 会据此生成 Display 实现。它支持单元变体、元组变体、具名字段变体以及结构体错误类型。(Docs.rs)
1. 不带字段的错误
use thiserror::Error;
#[derive(Debug, Error)]
enum ConfigError {
#[error("配置文件不存在")]
FileNotFound,
#[error("配置内容为空")]
Empty,
}
使用:
fn load_config() -> Result<(), ConfigError> {
Err(ConfigError::FileNotFound)
}
fn main() {
let error = load_config().unwrap_err();
println!("{error}");
}
输出:
配置文件不存在
这种变体适合不需要附加上下文数据的错误。
2. 元组字段:使用 {0}、{1}
use thiserror::Error;
#[derive(Debug, Error)]
enum UserError {
#[error("用户不存在,用户 ID:{0}")]
NotFound(u64),
#[error("字段 `{0}` 的值 `{1}` 不合法")]
InvalidField(String, String),
}
使用:
fn validate_age(age: i32) -> Result<(), UserError> {
if age < 0 {
return Err(UserError::InvalidField(
"age".to_string(),
age.to_string(),
));
}
Ok(())
}
对应关系:
InvalidField(String, String)
│ │
│ └── {1}
└────────── {0}
3. 具名字段
use thiserror::Error;
#[derive(Debug, Error)]
enum ValidationError {
#[error("字段 `{field}` 的值 `{value}` 不合法:{reason}")]
InvalidValue {
field: String,
value: String,
reason: String,
},
}
创建错误:
fn validate_email(email: &str) -> Result<(), ValidationError> {
if !email.contains('@') {
return Err(ValidationError::InvalidValue {
field: "email".to_string(),
value: email.to_string(),
reason: "缺少 @ 符号".to_string(),
});
}
Ok(())
}
调用:
fn main() {
if let Err(error) = validate_email("abc.example.com") {
println!("{error}");
}
}
输出:
字段 `email` 的值 `abc.example.com` 不合法:缺少 @ 符号
具名字段通常比多个元组字段可读性更好。
例如:
InvalidValue(String, String, String)
不如:
InvalidValue {
field: String,
value: String,
reason: String,
}
4. 使用格式化参数
#[error(...)] 使用的是 Rust 格式化语法,因此支持:
{}
{:?}
{:#?}
{:x}
{:02}
示例:
use thiserror::Error;
#[derive(Debug, Error)]
enum ProtocolError {
#[error("收到未知状态码:0x{code:x}")]
UnknownCode { code: u32 },
#[error("请求头不合法,期望值:{expected:?},实际值:{actual:?}")]
InvalidHeader {
expected: Vec<String>,
actual: Vec<String>,
},
}
四、在函数中返回 thiserror 错误
自定义错误类型通常和 Result<T, E> 配合:
use thiserror::Error;
#[derive(Debug, Error)]
enum LoginError {
#[error("用户名不能为空")]
EmptyUsername,
#[error("密码长度不能少于 {minimum} 位,实际为 {actual} 位")]
PasswordTooShort {
minimum: usize,
actual: usize,
},
#[error("用户名或密码错误")]
InvalidCredentials,
}
fn login(username: &str, password: &str) -> Result<(), LoginError> {
if username.trim().is_empty() {
return Err(LoginError::EmptyUsername);
}
if password.len() < 8 {
return Err(LoginError::PasswordTooShort {
minimum: 8,
actual: password.len(),
});
}
if username != "admin" || password != "12345678" {
return Err(LoginError::InvalidCredentials);
}
Ok(())
}
fn main() {
match login("admin", "123") {
Ok(()) => println!("登录成功"),
Err(error) => println!("登录失败:{error}"),
}
}
输出:
登录失败:密码长度不能少于 8 位,实际为 3 位
五、#[from]:自动转换底层错误
这是 thiserror 最常用的功能之一。
假设读取文件时,底层返回:
std::io::Error
解析整数时,底层返回:
std::num::ParseIntError
我们希望业务函数统一返回:
ConfigError
可以使用 #[from]。
1. 基本示例
use std::fs;
use std::num::ParseIntError;
use thiserror::Error;
#[derive(Debug, Error)]
enum ConfigError {
#[error("读取配置文件失败:{0}")]
Io(#[from] std::io::Error),
#[error("端口格式错误:{0}")]
InvalidPort(#[from] ParseIntError),
}
fn read_port(path: &str) -> Result<u16, ConfigError> {
let content = fs::read_to_string(path)?;
let port = content.trim().parse::<u16>()?;
Ok(port)
}
这里:
fs::read_to_string(path)?
返回类型是:
Result<String, std::io::Error>
但是当前函数要求:
Result<u16, ConfigError>
由于写了:
Io(#[from] std::io::Error)
thiserror 会生成大致如下实现:
impl From<std::io::Error> for ConfigError {
fn from(error: std::io::Error) -> Self {
ConfigError::Io(error)
}
}
所以 ? 可以自动完成:
std::io::Error
↓ From
ConfigError::Io
同理:
content.trim().parse::<u16>()?
会把:
ParseIntError
转换成:
ConfigError::InvalidPort
#[from] 字段还会被视为错误来源,因此通常不需要再同时写 #[source]。(Docs.rs)
2. 等价的手动写法
没有 #[from] 时:
fn read_port(path: &str) -> Result<u16, ConfigError> {
let content = fs::read_to_string(path)
.map_err(ConfigError::Io)?;
let port = content
.trim()
.parse::<u16>()
.map_err(ConfigError::InvalidPort)?;
Ok(port)
}
使用 #[from] 后:
fn read_port(path: &str) -> Result<u16, ConfigError> {
let content = fs::read_to_string(path)?;
let port = content.trim().parse::<u16>()?;
Ok(port)
}
3. #[from] 的字段限制
通常带 #[from] 的变体只能包含:
- 一个来源错误字段;
- 或来源错误字段加一个特殊的 backtrace 字段。
例如下面是标准写法:
#[derive(Debug, thiserror::Error)]
enum AppError {
#[error("I/O 操作失败")]
Io(#[from] std::io::Error),
}
但是下面这种写法不适合直接使用 #[from]:
#[derive(Debug, thiserror::Error)]
enum AppError {
#[error("读取文件 `{path}` 失败:{source}")]
ReadFile {
path: String,
#[from]
source: std::io::Error,
},
}
因为从一个单独的 std::io::Error 无法自动得到 path。
这时应使用 #[source],并手动构造错误:
use std::path::PathBuf;
use thiserror::Error;
#[derive(Debug, Error)]
enum AppError {
#[error("读取文件 `{path}` 失败:{source}")]
ReadFile {
path: PathBuf,
#[source]
source: std::io::Error,
},
}
函数中:
use std::fs;
use std::path::Path;
fn read_file(path: &Path) -> Result<String, AppError> {
fs::read_to_string(path).map_err(|source| AppError::ReadFile {
path: path.to_path_buf(),
source,
})
}
这比只返回:
No such file or directory
更有业务上下文:
读取文件 `/data/config.toml` 失败:No such file or directory
六、#[source]:建立错误来源链
#[source] 表示某个字段是当前错误的底层原因。
例如:
use std::io;
use thiserror::Error;
#[derive(Debug, Error)]
enum ServiceError {
#[error("加载用户配置失败:{source}")]
LoadConfig {
#[source]
source: io::Error,
},
}
thiserror 会生成类似:
impl std::error::Error for ServiceError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
ServiceError::LoadConfig { source } => Some(source),
}
}
}
1. 查看错误链
use std::error::Error;
use std::fs;
use std::path::PathBuf;
use thiserror::Error;
#[derive(Debug, Error)]
enum AppError {
#[error("读取配置文件 `{path}` 失败")]
ReadConfig {
path: PathBuf,
#[source]
source: std::io::Error,
},
}
fn read_config(path: &str) -> Result<String, AppError> {
fs::read_to_string(path).map_err(|source| AppError::ReadConfig {
path: PathBuf::from(path),
source,
})
}
fn print_error_chain(error: &(dyn Error + 'static)) {
eprintln!("错误:{error}");
let mut current = error.source();
while let Some(source) = current {
eprintln!("原因:{source}");
current = source.source();
}
}
fn main() {
if let Err(error) = read_config("/data/not-exist.toml") {
print_error_chain(&error);
}
}
输出类似:
错误:读取配置文件 `/data/not-exist.toml` 失败 原因:No such file or directory (os error 2)
2. 名为 source 的字段可以自动识别
官方文档说明,字段名直接叫作 source 时,可以作为错误来源被识别。显式写 #[source] 通常更清楚。(Docs.rs)
例如:
#[derive(Debug, thiserror::Error)]
enum AppError {
#[error("数据库连接失败")]
Database {
source: std::io::Error,
},
}
建议写成:
#[derive(Debug, thiserror::Error)]
enum AppError {
#[error("数据库连接失败")]
Database {
#[source]
source: std::io::Error,
},
}
可读性更强。
七、#[error(transparent)]:透明包装错误
透明错误表示:
当前错误只是包装底层错误,不额外改变其显示内容和错误来源行为。
示例:
use thiserror::Error;
#[derive(Debug, Error)]
enum AppError {
#[error(transparent)]
Io(#[from] std::io::Error),
}
假设底层错误是:
No such file or directory (os error 2)
打印 AppError 时仍然显示:
No such file or directory (os error 2)
而不是:
I/O 错误:No such file or directory
什么时候使用 transparent
适合以下场景:
- 只是统一错误返回类型;
- 不希望改变底层错误文本;
- 新类型包装模式;
- 需要隐藏内部错误实现。
例如:
use thiserror::Error;
#[derive(Debug, Error)]
#[error(transparent)]
pub struct PublicError(#[from] ErrorRepr);
#[derive(Debug, Error)]
enum ErrorRepr {
#[error("配置文件不存在")]
ConfigNotFound,
#[error("数据库连接失败:{0}")]
Database(String),
}
调用方只能看到:
PublicError
内部可以自由调整:
ErrorRepr
这种公共错误包装模式也出现在 thiserror 官方文档中。(Docs.rs)
八、结构体错误类型
错误不一定必须定义成枚举,也可以是结构体。
1. 普通结构体错误
use thiserror::Error;
#[derive(Debug, Error)]
#[error("参数 `{parameter}` 不合法:{reason}")]
struct InvalidParameterError {
parameter: String,
reason: String,
}
使用:
fn set_port(port: u16) -> Result<(), InvalidParameterError> {
if port == 0 {
return Err(InvalidParameterError {
parameter: "port".to_string(),
reason: "端口不能为 0".to_string(),
});
}
Ok(())
}
2. 元组结构体错误
use thiserror::Error;
#[derive(Debug, Error)]
#[error("用户不存在:{0}")]
struct UserNotFoundError(u64);
3. 单元结构体错误
use thiserror::Error;
#[derive(Debug, Error)]
#[error("权限不足")]
struct PermissionDeniedError;
枚举还是结构体
只有一种错误时,可以使用结构体:
struct UserNotFoundError;
一个模块存在多种错误时,通常使用枚举:
enum UserServiceError {
NotFound,
InvalidEmail,
Database,
}
九、完整案例:读取应用配置
下面把几个核心功能组合起来。
1. 项目配置
Cargo.toml:
[package]
name = "config-demo"
version = "0.1.0"
edition = "2024"
[dependencies]
thiserror = "2"
serde = { version = "1", features = ["derive"] }
toml = "0.8"
2. 配置文件
config.toml:
host = "127.0.0.1" port = 8080 workers = 4
3. Rust 代码
use serde::Deserialize;
use std::fs;
use std::path::{Path, PathBuf};
use thiserror::Error;
#[derive(Debug, Deserialize)]
struct AppConfig {
host: String,
port: u16,
workers: usize,
}
#[derive(Debug, Error)]
enum ConfigError {
#[error("读取配置文件 `{path}` 失败")]
ReadFile {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("解析配置文件 `{path}` 失败")]
ParseFile {
path: PathBuf,
#[source]
source: toml::de::Error,
},
#[error("配置项 workers 必须大于 0")]
InvalidWorkers,
#[error("端口不能为 0")]
InvalidPort,
}
fn load_config(path: impl AsRef<Path>) -> Result<AppConfig, ConfigError> {
let path = path.as_ref();
let content = fs::read_to_string(path).map_err(|source| {
ConfigError::ReadFile {
path: path.to_path_buf(),
source,
}
})?;
let config: AppConfig = toml::from_str(&content).map_err(|source| {
ConfigError::ParseFile {
path: path.to_path_buf(),
source,
}
})?;
if config.workers == 0 {
return Err(ConfigError::InvalidWorkers);
}
if config.port == 0 {
return Err(ConfigError::InvalidPort);
}
Ok(config)
}
fn main() {
match load_config("config.toml") {
Ok(config) => {
println!("配置加载成功:{config:#?}");
println!(
"监听地址:http://{}:{},工作线程:{}",
config.host,
config.port,
config.workers
);
}
Err(error) => {
eprintln!("启动失败:{error}");
let mut source = std::error::Error::source(&error);
while let Some(cause) = source {
eprintln!("底层原因:{cause}");
source = cause.source();
}
}
}
}
这个案例有三个层次:
系统错误 └── std::io::Error 解析错误 └── toml::de::Error 业务校验错误 ├── InvalidWorkers └── InvalidPort
这正是 thiserror 比单纯字符串错误更有价值的地方:调用方可以根据错误变体进行精确处理。
十、根据错误变体执行不同逻辑
定义:
use thiserror::Error;
#[derive(Debug, Error)]
enum UserError {
#[error("用户不存在:{0}")]
NotFound(u64),
#[error("用户已被禁用:{0}")]
Disabled(u64),
#[error("数据库错误:{0}")]
Database(String),
}
处理:
fn handle_error(error: UserError) {
match error {
UserError::NotFound(user_id) => {
println!("返回 HTTP 404,用户 ID:{user_id}");
}
UserError::Disabled(user_id) => {
println!("返回 HTTP 403,用户 ID:{user_id}");
}
UserError::Database(message) => {
eprintln!("返回 HTTP 500,数据库错误:{message}");
}
}
}
如果只返回:
Err("用户不存在".to_string())
调用方只能比较字符串:
if error == "用户不存在"
这非常脆弱。
而枚举可以:
match error {
UserError::NotFound(id) => {}
UserError::Disabled(id) => {}
UserError::Database(message) => {}
}
十一、为错误增加业务上下文
只使用 #[from]:
#[derive(Debug, thiserror::Error)]
enum AppError {
#[error("I/O 错误:{0}")]
Io(#[from] std::io::Error),
}
调用:
let content = std::fs::read_to_string(path)?;
最终只知道:
I/O 错误:No such file or directory
不知道是哪个文件。
更好的设计:
use std::path::PathBuf;
use thiserror::Error;
#[derive(Debug, Error)]
enum AppError {
#[error("读取文件 `{path}` 失败")]
ReadFile {
path: PathBuf,
#[source]
source: std::io::Error,
},
}
然后手动补充上下文:
fn read_file(path: &str) -> Result<String, AppError> {
std::fs::read_to_string(path).map_err(|source| {
AppError::ReadFile {
path: PathBuf::from(path),
source,
}
})
}
错误显示:
读取文件 `/data/app.toml` 失败
错误链:
No such file or directory (os error 2)
设计错误时可以遵循一个原则:
错误变体负责说明“业务操作失败” source 负责保存“底层技术原因”
例如:
创建用户失败 └── 数据库唯一索引冲突
而不是只把所有错误原样向上传递。
十二、模块化项目中的错误设计
对于多模块项目,不建议所有错误都塞进一个巨大枚举:
enum AppError {
UserNotFound,
UserDisabled,
OrderNotFound,
OrderPaid,
ConfigMissing,
DatabaseError,
RedisError,
IoError,
// 几百个变体……
}
更推荐每个模块有自己的错误类型。
1. 目录结构
src/
├── main.rs
├── error.rs
├── user/
│ ├──.rs
│ ├── error.rs
│ └── service.rs
└── order/
├──.rs
├── error.rs
└── service.rs
Rust 2024 项目不必使用旧式 mod.rs。
2. 用户模块错误
src/user/error.rs:
use thiserror::Error;
#[derive(Debug, Error)]
pub enum UserError {
#[error("用户不存在:{0}")]
NotFound(u64),
#[error("邮箱地址不合法:{0}")]
InvalidEmail(String),
#[error("用户已被禁用:{0}")]
Disabled(u64),
}
src/user.rs:
pub mod error; pub mod service;
src/user/service.rs:
use super::error::UserError;
pub fn find_user(user_id: u64) -> Result<String, UserError> {
if user_id != 1 {
return Err(UserError::NotFound(user_id));
}
Ok("Albert".to_string())
}
3. 应用层统一错误
src/error.rs:
use crate::user::error::UserError;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum AppError {
#[error(transparent)]
User(#[from] UserError),
#[error("系统内部错误:{0}")]
Internal(String),
}
业务函数:
use crate::error::AppError;
use crate::user::service::find_user;
fn execute() -> Result<(), AppError> {
let username = find_user(100)?;
println!("用户:{username}");
Ok(())
}
转换链:
UserError::NotFound(100)
↓ #[from]
AppError::User(...)
十三、Web 项目中的使用方式
以 Salvo、Axum、Actix Web 等 Web 框架为例,通常需要把业务错误转换为 HTTP 响应。
可以将错误类型分成两部分:
领域错误
↓
应用错误
↓
HTTP 响应
示例:
use thiserror::Error;
#[derive(Debug, Error)]
enum UserServiceError {
#[error("用户不存在:{0}")]
NotFound(u64),
#[error("用户无权执行该操作")]
PermissionDenied,
#[error("数据库操作失败")]
Database {
#[source]
source: std::io::Error,
},
}
映射 HTTP 状态码:
impl UserServiceError {
fn status_code(&self) -> u16 {
match self {
UserServiceError::NotFound(_) => 404,
UserServiceError::PermissionDenied => 403,
UserServiceError::Database { .. } => 500,
}
}
fn public_message(&self) -> &'static str {
match self {
UserServiceError::NotFound(_) => "用户不存在",
UserServiceError::PermissionDenied => "没有操作权限",
UserServiceError::Database { .. } => "系统内部错误",
}
}
}
这里需要区分:
内部日志错误: 数据库操作失败:Connection refused 返回给前端: 系统内部错误
不要直接把数据库地址、SQL、文件路径等内部信息返回给客户端。
十四、thiserror 和 anyhow 的区别
这是最容易混淆的地方。
thiserror
用于自定义错误类型:
#[derive(Debug, thiserror::Error)]
enum UserError {
#[error("用户不存在:{0}")]
NotFound(u64),
#[error("数据库错误")]
Database,
}
特点:
- 错误是结构化的;
- 可以
match; - 适合库、模块边界和领域层;
- 适合公开 API;
- 调用方可以判断具体错误类型。
anyhow
用于方便地传播和补充错误上下文:
use anyhow::{Context, Result};
fn load_config() -> Result<String> {
let content = std::fs::read_to_string("config.toml")
.context("读取 config.toml 失败")?;
Ok(content)
}
特点:
- 统一使用
anyhow::Error; - 使用方便;
- 容易添加上下文;
- 通常适合应用入口、命令行程序和不要求调用方匹配错误变体的上层代码。
thiserror 官方文档也将 anyhow 作为应用代码中“单一方便错误类型”的互补工具。(Docs.rs)
推荐配合方式
底层模块、领域模块、公共库
使用 thiserror
应用入口、main、任务调度、CLI
使用 anyhow
例如:
use anyhow::{Context, Result};
use thiserror::Error;
#[derive(Debug, Error)]
enum ConfigError {
#[error("配置文件不存在")]
NotFound,
#[error("配置格式错误:{0}")]
InvalidFormat(String),
}
fn parse_config(content: &str) -> Result<(), ConfigError> {
if content.trim().is_empty() {
return Err(ConfigError::InvalidFormat(
"配置内容为空".to_string(),
));
}
Ok(())
}
fn run() -> Result<()> {
let content = std::fs::read_to_string("config.toml")
.context("读取主配置文件失败")?;
parse_config(&content)
.context("解析应用配置失败")?;
Ok(())
}
fn main() {
if let Err(error) = run() {
eprintln!("{error:#}");
}
}
这里:
parse_config()
返回 ConfigError
使用 thiserror
run()
返回 anyhow::Result
使用 anyhow 添加上下文
十五、多个相同底层错误时的处理
下面这种定义会产生冲突:
#[derive(Debug, thiserror::Error)]
enum AppError {
#[error("读取配置失败:{0}")]
ReadConfig(#[from] std::io::Error),
#[error("写入日志失败:{0}")]
WriteLog(#[from] std::io::Error),
}
原因是它相当于想生成两份:
impl From<std::io::Error> for AppError
Rust 无法根据同一个 std::io::Error 自动判断应该转换为:
ReadConfig
还是:
WriteLog
正确方式是去掉 #[from]:
use thiserror::Error;
#[derive(Debug, Error)]
enum AppError {
#[error("读取配置失败")]
ReadConfig {
#[source]
source: std::io::Error,
},
#[error("写入日志失败")]
WriteLog {
#[source]
source: std::io::Error,
},
}
然后明确映射:
fn read_config() -> Result<String, AppError> {
std::fs::read_to_string("config.toml")
.map_err(|source| AppError::ReadConfig { source })
}
fn write_log(content: &str) -> Result<(), AppError> {
std::fs::write("app.log", content)
.map_err(|source| AppError::WriteLog { source })
}
十六、保存额外上下文数据
错误类型不仅用于打印,还可以保存程序处理所需的信息。
use thiserror::Error;
#[derive(Debug, Error)]
enum ApiError {
#[error("远程服务请求失败,状态码:{status_code},请求 ID:{request_id}")]
RemoteService {
status_code: u16,
request_id: String,
response_body: String,
},
}
调用方可以提取字段:
fn handle_error(error: ApiError) {
match error {
ApiError::RemoteService {
status_code,
request_id,
response_body,
} => {
println!("状态码:{status_code}");
println!("请求 ID:{request_id}");
println!("响应内容:{response_body}");
}
}
}
注意:
#[error(...)]
只决定错误怎么显示,并不会影响字段本身。
十七、泛型错误类型
thiserror 也可以用于泛型:
use thiserror::Error;
#[derive(Debug, Error)]
pub enum RepositoryError<E>
where
E: std::error::Error + 'static,
{
#[error("记录不存在")]
NotFound,
#[error("存储层错误:{0}")]
Storage(E),
}
不过在实际业务项目中,泛型错误容易使类型签名变复杂。
通常更常见的是明确指定底层错误:
#[derive(Debug, thiserror::Error)]
enum UserRepositoryError {
#[error("用户不存在")]
NotFound,
#[error("数据库错误:{0}")]
Database(String),
}
或者:
#[derive(Debug, thiserror::Error)]
enum UserRepositoryError {
#[error("数据库错误")]
Database {
#[source]
source: sea_orm::DbErr,
},
}
十八、SeaORM 项目示例
你的常用技术栈包括 SeaORM,因此可以这样设计。
use sea_orm::DbErr;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum UserRepositoryError {
#[error("用户不存在:{0}")]
NotFound(i64),
#[error("查询用户失败")]
Query {
#[source]
source: DbErr,
},
#[error("创建用户失败")]
Create {
#[source]
source: DbErr,
},
#[error("更新用户失败")]
Update {
#[source]
source: DbErr,
},
}
查询:
use sea_orm::{DatabaseConnection, EntityTrait};
async fn find_user(
db: &DatabaseConnection,
user_id: i64,
) -> Result<user::Model, UserRepositoryError> {
let result = user::Entity::find_by_id(user_id)
.one(db)
.await
.map_err(|source| UserRepositoryError::Query { source })?;
result.ok_or(UserRepositoryError::NotFound(user_id))
}
这里没有直接写:
Database(#[from] DbErr)
而是区分成:
Query Create Update
这样发生错误时,能够知道具体哪个数据库操作失败。
简单项目也可以统一:
#[derive(Debug, thiserror::Error)]
pub enum RepositoryError {
#[error("数据库操作失败:{0}")]
Database(#[from] sea_orm::DbErr),
}
两种方式的区别是:
统一 Database
代码简单,但业务上下文较少
Query/Create/Update
代码稍多,但日志定位更准确
十九、Redis 错误示例
use thiserror::Error;
#[derive(Debug, Error)]
pub enum CacheError {
#[error("Redis 连接失败")]
Connection {
#[source]
source: redis::RedisError,
},
#[error("读取缓存失败,key:{key}")]
Get {
key: String,
#[source]
source: redis::RedisError,
},
#[error("写入缓存失败,key:{key}")]
Set {
key: String,
#[source]
source: redis::RedisError,
},
}
使用:
async fn get_cache(
connection: &mut redis::aio::MultiplexedConnection,
key: &str,
) -> Result<Option<String>, CacheError> {
use redis::AsyncCommands;
connection
.get(key)
.await
.map_err(|source| CacheError::Get {
key: key.to_string(),
source,
})
}
二十、错误测试
建议同时测试错误变体和显示文本。
use thiserror::Error;
#[derive(Debug, Error)]
enum UserError {
#[error("用户不存在:{0}")]
NotFound(u64),
#[error("邮箱不合法:{0}")]
InvalidEmail(String),
}
fn find_user(id: u64) -> Result<(), UserError> {
Err(UserError::NotFound(id))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn should_return_not_found() {
let result = find_user(100);
assert!(matches!(
result,
Err(UserError::NotFound(100))
));
}
#[test]
fn should_format_error_message() {
let error = UserError::NotFound(100);
assert_eq!(
error.to_string(),
"用户不存在:100"
);
}
}
对于带 source 的错误:
#[cfg(test)]
mod tests {
use std::error::Error;
use super::*;
#[test]
fn should_keep_source_error() {
let source = std::io::Error::new(
std::io::ErrorKind::NotFound,
"文件不存在",
);
let error = AppError::ReadFile {
path: "config.toml".into(),
source,
};
assert!(error.source().is_some());
}
}
二十一、#[backtrace]
thiserror 支持 #[backtrace] 属性,用于将错误中的 backtrace 通过 Error::provide() 暴露出去。不过官方文档注明,该属性目前涉及 nightly 编译器能力;普通 stable 项目不要把它作为基础方案依赖。(crates.io)
形式类似:
use std::backtrace::Backtrace;
use thiserror::Error;
#[derive(Debug, Error)]
enum AppError {
#[error("I/O 操作失败")]
Io {
source: std::io::Error,
#[backtrace]
backtrace: Backtrace,
},
}
普通项目更常见的做法是:
- 使用
anyhow捕获和展示错误链; - 设置
RUST_BACKTRACE=1; - 使用
tracing记录错误上下文; - 在错误发生的位置记录调用参数。
Linux/macOS:
RUST_BACKTRACE=1 cargo run
PowerShell:
$env:RUST_BACKTRACE = "1" cargo run
二十二、错误可见性设计
假设你正在开发一个库:
pub fn load_user() -> Result<User, UserError>
那么 UserError 就是公开 API 的一部分:
#[derive(Debug, thiserror::Error)]
pub enum UserError {
#[error("用户不存在")]
NotFound,
#[error("数据库错误:{0}")]
Database(String),
}
调用方可能这样写:
match load_user() {
Err(UserError::NotFound) => {}
Err(UserError::Database(_)) => {}
Ok(user) => {}
}
因此,一旦新增错误变体,可能影响调用方的穷尽匹配。
公共库可以考虑添加:
#[non_exhaustive]
#[derive(Debug, thiserror::Error)]
pub enum UserError {
#[error("用户不存在")]
NotFound,
#[error("数据库错误")]
Database,
}
调用方必须增加兜底:
match error {
UserError::NotFound => {}
UserError::Database => {}
_ => {}
}
这有利于库未来新增错误变体。
二十三、常见错误与注意事项
1. 忘记派生 Debug
错误类型必须满足 std::error::Error 所要求的调试能力。
错误写法:
#[derive(thiserror::Error)]
enum AppError {
#[error("错误")]
Failed,
}
推荐:
#[derive(Debug, thiserror::Error)]
enum AppError {
#[error("错误")]
Failed,
}
2. 错误类型中全部使用 String
不推荐:
#[derive(Debug, thiserror::Error)]
enum AppError {
#[error("系统错误:{0}")]
System(String),
}
然后什么错误都转字符串:
.map_err(|error| AppError::System(error.to_string()))
这会丢失:
- 原始错误类型;
source()错误链;- downcast 能力;
- 部分调试信息。
更好:
#[derive(Debug, thiserror::Error)]
enum AppError {
#[error("I/O 错误")]
Io {
#[source]
source: std::io::Error,
},
}
3. 错误文本缺少业务语义
不够好:
#[error("{0}")]
Database(#[from] sea_orm::DbErr)
稍好:
#[error("数据库操作失败:{0}")]
Database(#[from] sea_orm::DbErr)
更适合大型项目:
#[error("查询用户失败")]
QueryUser {
#[source]
source: sea_orm::DbErr,
}
4. 将敏感信息直接放入 Display
谨慎使用:
#[error("数据库连接失败:用户名={username},密码={password}")]
DatabaseConnection {
username: String,
password: String,
}
密码、Token、密钥不应出现在错误文本和日志中。
应该写:
#[error("数据库连接失败,主机:{host}")]
DatabaseConnection {
host: String,
#[source]
source: DatabaseError,
}
5. 把所有错误都做成 #[from]
虽然这样很方便:
#[derive(Debug, thiserror::Error)]
enum AppError {
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
Database(#[from] sea_orm::DbErr),
#[error(transparent)]
Redis(#[from] redis::RedisError),
}
但是它缺少业务上下文。
对于原型、小工具可以这样做。
对于正式业务系统,建议按操作分类:
ReadConfig CreateUser UpdateOrder ReadCache WriteCache
二十四、推荐的错误分层方案
结合 Rust Web 项目,可以使用四层错误设计:
基础设施错误
SeaORM、Redis、I/O、HTTP 客户端错误
↓
仓储层错误
UserRepositoryError
↓
服务层错误
UserServiceError
↓
接口层响应
HTTP 400 / 403 / 404 / 500
示例:
use thiserror::Error;
#[derive(Debug, Error)]
pub enum UserRepositoryError {
#[error("用户记录不存在:{0}")]
NotFound(i64),
#[error("数据库查询失败")]
Query {
#[source]
source: sea_orm::DbErr,
},
}
#[derive(Debug, Error)]
pub enum UserServiceError {
#[error("用户不存在:{0}")]
NotFound(i64),
#[error("用户已被禁用:{0}")]
Disabled(i64),
#[error("用户存储服务异常")]
Repository {
#[source]
source: UserRepositoryError,
},
}
转换:
impl From<UserRepositoryError> for UserServiceError {
fn from(error: UserRepositoryError) -> Self {
match error {
UserRepositoryError::NotFound(user_id) => {
UserServiceError::NotFound(user_id)
}
other => {
UserServiceError::Repository {
source: other,
}
}
}
}
}
这里没有直接写:
Repository(#[from] UserRepositoryError)
是因为需要把仓储层的:
UserRepositoryError::NotFound
映射成服务层的:
UserServiceError::NotFound
而其他仓储错误统一包装。
这体现了一个重要原则:
#[from] 适合无条件的一对一转换。 手写 From 适合需要判断、分类或改变语义的转换。
二十五、完整可运行综合示例
use std::error::Error;
use std::fs;
use std::num::ParseIntError;
use std::path::{Path, PathBuf};
use thiserror::Error;
#[derive(Debug, Error)]
enum ConfigError {
#[error("读取配置文件 `{path}` 失败")]
ReadFile {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("配置文件内容为空")]
Empty,
#[error("端口格式错误:{0}")]
InvalidPort(#[from] ParseIntError),
#[error("端口不能为 0")]
ZeroPort,
}
fn load_port(path: impl AsRef<Path>) -> Result<u16, ConfigError> {
let path = path.as_ref();
let content = fs::read_to_string(path).map_err(|source| {
ConfigError::ReadFile {
path: path.to_path_buf(),
source,
}
})?;
let content = content.trim();
if content.is_empty() {
return Err(ConfigError::Empty);
}
let port = content.parse::<u16>()?;
if port == 0 {
return Err(ConfigError::ZeroPort);
}
Ok(port)
}
fn print_error(error: &(dyn Error + 'static)) {
eprintln!("操作失败:{error}");
let mut index = 1;
let mut source = error.source();
while let Some(cause) = source {
eprintln!("原因 {index}:{cause}");
index += 1;
source = cause.source();
}
}
fn main() {
match load_port("port.txt") {
Ok(port) => {
println!("服务端口:{port}");
}
Err(error) => {
print_error(&error);
match error {
ConfigError::ReadFile { path, .. } => {
eprintln!("请检查文件是否存在:{}", path.display());
}
ConfigError::Empty => {
eprintln!("请在文件中填写端口,例如:8080");
}
ConfigError::InvalidPort(_) => {
eprintln!("端口必须是 0~65535 之间的整数");
}
ConfigError::ZeroPort => {
eprintln!("请使用非零端口");
}
}
}
}
}
测试不同情况:
echo 8080 > port.txt cargo run
输出:
服务端口:8080
写入错误内容:
echo abc > port.txt cargo run
输出类似:
操作失败:端口格式错误:invalid digit found in string 原因 1:invalid digit found in string 端口必须是 0~65535 之间的整数
删除文件:
rm port.txt cargo run
输出类似:
操作失败:读取配置文件 `port.txt` 失败 原因 1:No such file or directory (os error 2) 请检查文件是否存在:port.txt
二十六、核心属性速查表
| 属性 | 作用 | 示例 |
|---|---|---|
#[derive(Error)] |
自动实现 Error |
#[derive(Debug, Error)] |
#[error("...")] |
自动实现 Display |
#[error("用户不存在:{0}")] |
#[from] |
自动实现 From<T>,支持 ? 转换 |
Io(#[from] io::Error) |
#[source] |
指定底层错误来源 | #[source] source: io::Error |
#[error(transparent)] |
透明转发底层错误显示和来源 | Io(#[from] io::Error) |
#[backtrace] |
暴露 backtrace | 当前需要谨慎考虑编译器支持 |
二十七、使用建议总结
小型工具可以直接统一包装:
#[derive(Debug, thiserror::Error)]
enum AppError {
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
ParseInt(#[from] std::num::ParseIntError),
}
正式业务系统建议保存业务上下文:
#[derive(Debug, thiserror::Error)]
enum AppError {
#[error("读取配置文件 `{path}` 失败")]
ReadConfig {
path: PathBuf,
#[source]
source: std::io::Error,
},
}
底层模块、领域层和公共库优先使用:
thiserror
应用入口、CLI、任务执行器可以使用:
anyhow
最推荐的组合是:
thiserror:定义“这是什么错误” anyhow:说明“执行什么操作时发生了这个错误”
例如:
解析应用配置失败
└── 端口格式错误
└── invalid digit found in string
这能同时获得结构化处理能力、清晰的业务语义和完整的底层错误链。