Rust自定义错误和thiserror使用
Rust 自定义错误类型
一、核心四要素
自定义错误类型需要 四个部分,每个回答不同的问题:
graph TD
A["① #[derive(Debug)]<br/>pub enum MyError { ... }<br/><i>unwrap / main / 调试输出常需要 E: Debug</i>"]
B["② Display<br/><br/>发生了什么?<br/>给人类看的描述<br/><i>println!("{}")</i>"]
C["③ Error::source<br/><br/>根因是什么?<br/>建立错误溯源链<br/><i>e.source()</i>"]
D["④ From<X><br/><br/>怎么把底层错误<br/>自动包进来?<br/><i>? 操作符用</i>"]
A --> B
A --> C
A --> D
| 要素 | Trait | 给谁用 | 回答的问题 |
|---|---|---|---|
| ① Debug | Debug |
编译器 / unwrap / main / 调试输出 |
错误能不能用 {:?} 打印? |
| ② Display | Display |
人类 / 日志 | "发生了什么?" |
| ③ source | Error |
上层代码 | "根因是什么?" |
| ④ From | From<T> |
? 操作符 |
"怎么把底层错误包进来?" |
二、各要素详解
① Debug - 打印错误信息
#[derive(Debug)]的作用是:让你的错误类型可以用{:?}调试打印
案例
#[derive(Debug)]
enum MyError {
// 自定义错误
NameErr(String),
// 其他错误类型包装
Io(std::io::Error),
}
fn test_err() -> Result<String, MyError> {
Err(MyError::NameErr("name err".to_string()))
}
fn test_io_err() -> Result<String, MyError> {
Err(MyError::Io(std::io::Error::new(
std::io::ErrorKind::Other,
"io error",
)))
}
fn print_err(result: Result<String, MyError>) {
match result {
Ok(_) => {}
Err(err) => {
// 打印错误
println!("{:?}", err);
}
}
}
fn main() {
let err = test_err();
print_err(err);
let io_err = test_io_err();
print_err(io_err);
}
NameErr("name err")
Io(Custom { kind: Other, error: "io error" })
② Display — "给人类看的描述"
和Debug对比
#[derive(Debug)]偏向于把内部结构打印出来println!("{:?}", err);
Display它更偏向“人类友好描述”println!("{}", err);
调用时机
println!("{}", e)、format!("{}", e)、日志宏等。
案例
// 需要导入std::fmt
use std::fmt;
#[derive(Debug)]
enum MyError {
NameErr(String),
Io(std::io::Error),
}
// 定义fmt::Display
impl fmt::Display for MyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MyError::NameErr(msg) => write!(f, "名称错误: {}", msg),
MyError::Io(e) => write!(f, "IO 错误: {}", e),
}
}
}
fn test_err() -> Result<String, MyError> {
Err(MyError::NameErr("name err".to_string()))
}
fn test_io_err() -> Result<String, MyError> {
Err(MyError::Io(std::io::Error::new(
std::io::ErrorKind::Other,
"io error",
)))
}
fn print_err(result: Result<String, MyError>) {
match result {
Ok(_) => {}
Err(err) => {
// 需要定义fmt::Display才能使用{}打印错误
println!("{}", err);
}
}
}
fn main() {
let err = test_err();
print_err(err);
let io_err = test_io_err();
print_err(io_err);
}
名称错误: name err
IO 错误: io error
③ Error::source() — "根因是什么"
告诉上层:当前错误是由哪个更底层的错误引起的。
作用
建立错误链,让调用方能一层层追到根因。
MyError::Io(业务包装:"IO异常: xxx") ← Display 描述表象
└─ source() ──→ io::Error("No such file") ← source 指向根因
如果是[纯业务错误](没有包装其他错误),空实现即可:
impl Error for MyError {} // source() 默认返回 None,表示没有嵌套错误
案例
use std::{error::Error, fmt};
#[derive(Debug)]
enum MyError {
NameErr(String),
Io(std::io::Error), // 包装系统错误、第三方错误等
}
impl Error for MyError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
MyError::Io(e) => Some(e), // io::Error 是根因
_ => None, // 没有底层错误的返回None
}
}
}
impl fmt::Display for MyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MyError::NameErr(msg) => write!(f, "名称错误: {}", msg),
MyError::Io(e) => write!(f, "IO 错误: {}", e),
}
}
}
fn test_err() -> Result<String, MyError> {
// 返回Err(MyError)
Err(MyError::NameErr("name err".to_string()))
}
fn test_io_err() -> Result<String, MyError> {
Err(MyError::Io(std::io::Error::new(
std::io::ErrorKind::Other,
"io error",
)))
}
fn print_err(result: Result<String, MyError>) {
match result {
Ok(_) => {}
Err(err) => {
let mut err_str = format!("{}", err);
if let Some(source) = err.source() {
err_str.push_str(&format!(" -> 底层原因: {}", source));
}
println!("{}", err_str)
}
}
}
fn main() {
let err = test_err();
print_err(err);
let io_err = test_io_err();
print_err(io_err);
}
名称错误: name err
IO 错误: io error -> 底层原因: io error
source错误链追溯
不管嵌套多少层,都能完整追溯。不要用
e.source().source().source()硬编码,用循环
fn print_error_chain(mut err: &dyn Error) {
eprintln!("错误: {err}");
let mut depth = 1;
while let Some(src) = err.source() {
eprintln!(" 原因第{depth}层: {src}");
err = src;
depth += 1;
}
}
④ From<T> — "让 ? 自动转换"
定义From错误类型转换
#[derive(Debug)]
enum MyError {
Io(std::io::Error),
NumErr(ParseIntError)
}
impl From<std::io::Error> for MyError {
fn from(err: io::Error) -> Self {
MyError::Io(err)
}
}
impl From<ParseIntError> for MyError {
fn from(err: ParseIntError) -> Self {
MyError::NumErr(err)
}
}
作用
省掉每次手动
.map_err()。?遇到类型不匹配时,自动调用From::from()转换
// 有 From:? 自动转换
let content = std::fs::read_to_string(path)?; // io::Error → MyError
// 如果没有定义From:每次都要手动
let content = std::fs::read_to_string(path).map_err(MyError::Io)?;
案例
use std::{error::Error, fmt, num::ParseIntError};
#[derive(Debug)]
enum MyError {
NameErr(String),
Io(std::io::Error),
NumErr(ParseIntError)
}
impl Error for MyError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
MyError::Io(e) => Some(e), // std::io::Error 是根因
MyError::NumErr(e) => Some(e), // std::num::ParseIntError 是根因
_ => None,
}
}
}
impl fmt::Display for MyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
MyError::NameErr(msg) => write!(f, "名称错误: {}", msg),
MyError::Io(e) => write!(f, "IO 错误: {}", e),
MyError::NumErr(e) => write!(f, "数字错误: {}", e),
}
}
}
// 添加From转换
impl From<std::io::Error> for MyError {
fn from(err: std::io::Error) -> Self {
MyError::Io(err)
}
}
impl From<ParseIntError> for MyError {
fn from(err: ParseIntError) -> Self {
MyError::NumErr(err)
}
}
fn test_err() -> Result<String, MyError> {
// 返回Err(MyError)
Err(MyError::NameErr("name err".to_string()))
}
fn test_io_err() -> Result<String, MyError> {
let content = std::fs::read_to_string("not_exists.txt")?;
Ok(content)
}
fn test_num_err() -> Result<String, MyError> {
let num: i32 = "abc".parse()?;
Ok(num.to_string())
}
fn print_err(result: Result<String, MyError>) {
match result {
Ok(_) => {}
Err(err) => {
let mut err_str: String = format!("{}", err);
if let Some(source) = err.source() {
err_str.push_str(&format!(" -> 底层原因: {}", source));
}
println!("{}", err_str)
}
}
}
fn main() {
let err = test_err();
print_err(err);
let io_err = test_io_err();
print_err(io_err);
let num_err = test_num_err();
print_err(num_err);
}
名称错误: name err
IO 错误: No such file or directory (os error 2) -> 底层原因: No such file or directory (os error 2)
数字错误: invalid digit found in string -> 底层原因: invalid digit found in string
From和map_err对比
本质上干同一件事:把 Result<T, 底层错误> 变成 Result<T, MyError>,让 ? 能正常工作。
| - | impl From |
map_err |
|---|---|---|
| 写法 | 实现一次,? 自动转换 |
每次 ? 前手动调用 |
| 适用场景 | 同一转换多处使用 | 偶尔一两处需要转换 |
| 代码量 | 一次实现,调用处零成本 | 每次都要写 |
// 方案一:impl From — 推荐,同一转换在多处使用时
impl From<io::Error> for MyError {
fn from(err: io::Error) -> Self {
MyError::Io(err)
}
}
let content = std::fs::read_to_string(path)?; // 自动转换
// 方案二:map_err — 偶尔用
let content = std::fs::read_to_string(path).map_err(MyError::Io)?;
thiserror
thiserror 是 Rust 生态里非常常用的自定义错误类型辅助库
帮你为自定义错误类型自动生成Display、std::error::Error、source()、From<T>等样板代码
安装 thiserror
cargo add thiserror
thiserror 基本用法
Debug:允许{:?}调试打印Error:由thiserror自动实现std::error::Error
// 引入thiserror
use thiserror::Error;
// 定义错误类型
#[derive(Debug, Error)]
enum MyError {
#[error("名称错误: {0}")]
NameErr(String),
#[error("IO 错误: {0}")]
Io(#[from] std::io::Error),
#[error("数字错误: {0}")]
NumErr(#[from] std::num::ParseIntError),
}
thiserror 常用属性
#[error("...")]:生成 Display
元祖访问
{0}表示第0个字段
// 引入thiserror
use thiserror::Error;
// 定义错误类型
#[derive(Debug, Error)]
enum MyError {
...,
#[error("名称错误: {0}")]
NameErr(String)
#[error("位置错误: 字段={0}, 值={1}")]
IndexErr(String, i32)
}
- 相当于自动创建Display
impl std::fmt::Display for MyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MyError::NameErr(msg) => write!(f, "名称错误: {}", msg),
MyError::IndexErr(msg, index) => write!(f, "位置错误: 字段={}, 值={}", msg, index),
...
}
}
}
结构体变体的字段名
#[derive(Debug, Error)]
enum MyError {
...,
#[error("参数超出范围,最大值: {max}, 输入值: {input}")]
OutOfRange {
max: i32,
input: i32,
},
}
- 相当于自动创建Display
impl std::fmt::Display for MyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MyError::OutOfRange{ max, input } => write!(f, "参数超出范围,最大值: {}, 输入值: {}", max, input),
...
}
}
}
格式化输出
#[derive(Debug)]
struct User {
name: String,
age: i32,
}
#[derive(Debug)]
struct Location {
line: usize,
column: usize,
}
#[derive(Debug, Error)]
enum MyError {
#[error("用户错误: {0:?}")] // Debug输出
UserErr(User),
#[error("用户错误: {0:#?}")] // 漂亮的Debug输出
UserErr1(User),
#[error("比例错误: {0:.2}")] // 保留2位小数
RatioErr(f64),
#[error("错误码: 0x{0:x}")] // 十六进制输出
CodeErr(u32),
#[error("错误码: 0x{0:X}")] // 大写十六进制输出
CodeErr1(u32),
// 补齐
// println!("|{:04}|", 7);
#[error("错误码: {0:04}")] // 补零,最小宽度 4,输出 "0007"
ValueErr(u32),
// 对齐
// println!("|{:>5}|", 7);
#[error("值: {0:>5}")] // 右对齐,最小宽度 5,输出 " 7"
ValueErr1(i32),
// println!("|{:<5}|", 7);
#[error("值: {0:<5}")] // 左对齐,最小宽度 5,输出 "7 "
ValueErr2(i32),
// 结构体字段也可以用 Debug
#[error("配置错误: path={path}, source={source:?}")]
ConfigErr {
path: String,
source: std::io::Error,
},
// 访问成员属性
#[error("语法错误: line={}, column={}", .0.line, .0.column)]
Syntax(Location),
#[error("名称长度错误: 当前长度={}, 最大长度={max}", name.len())]
NameTooLong {
name: String,
max: usize,
},
// 转义
#[error("JSON 格式错误: 缺少字段 {{name}}")]
JsonErr,
// 透明转发
// Display 直接使用内部错误的 Display
// 同时 #[from] 会生成 From<T>,并把内部错误作为 source()
#[error(transparent)]
Io(#[from] sqlx::Error),
}
#[from]:生成From + source
// 引入thiserror
use thiserror::Error;
// 定义错误类型
#[derive(Debug, Error)]
enum MyError {
...,
#[error("IO 错误: {0}")]
Io(#[from] std::io::Error)
}
- 相当于自动创建From和source
// 自动生成From
impl From<std::io::Error> for MyError {
fn from(err: std::io::Error) -> Self {
MyError::Io(err)
}
}
// 自动生成source
impl std::error::Error for MyError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
MyError::Io(e) => Some(e),
_ => None,
}
}
}
#[source]:只标记根因,不生成From
#[derive(Debug, Error)]
enum MyError {
#[error("读取配置文件失败: {path}")]
ReadConfig {
path: String,
#[source]
source: std::io::Error,
},
}
- 只会创建source
// 不会生成 From<std::io::Error> for MyError
// 会生成source
impl Error for MyError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
MyError::ReadConfig { source, .. } => Some(source),
}
}
}
用法
fn read_config(path: &str) -> Result<String, MyError> {
let content = std::fs::read_to_string(path)
.map_err(|source| MyError::ReadConfig {
path: path.to_string(),
source, // 需要传入原始错误
})?;
Ok(content)
}
区别
| 属性 | 作用 | 是否生成 From<T> |
是否作为 source() |
|---|---|---|---|
#[from] |
自动转换底层错误 | 是 | 是 |
#[source] |
标记底层错误根因 | 否 | 是 |
自定义错误转换为thiserror
use std::num::ParseIntError;
use thiserror::Error;
#[derive(Debug, Error)]
enum MyError {
#[error("名称错误: {0}")]
NameErr(String),
#[error("IO 错误: {0}")]
Io(#[from] std::io::Error),
#[error("数字错误: {0}")]
NumErr(#[from] ParseIntError),
}
fn test_err() -> Result<String, MyError> {
Err(MyError::NameErr("name err".to_string()))
}
fn test_io_err() -> Result<String, MyError> {
let content = std::fs::read_to_string("not_exists.txt")?;
Ok(content)
}
fn test_num_err() -> Result<String, MyError> {
let num: i32 = "abc".parse()?;
Ok(num.to_string())
}
fn print_err(result: Result<String, MyError>) {
match result {
Ok(value) => {
println!("成功: {}", value);
}
Err(err) => {
let mut err_str = format!("{}", err);
if let Some(source) = err.source() {
err_str.push_str(&format!(" -> 底层原因: {}", source));
}
println!("{}", err_str);
}
}
}
fn main() {
print_err(test_err());
print_err(test_io_err());
print_err(test_num_err());
}
名称错误: name err
IO 错误: No such file or directory (os error 2) -> 底层原因: No such file or directory (os error 2)
数字错误: invalid digit found in string -> 底层原因: invalid digit found in string

浙公网安备 33010602011771号