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!(&quot;{}&quot;)</i>"] C["③ Error::source<br/><br/>根因是什么?<br/>建立错误溯源链<br/><i>e.source()</i>"] D["④ From&lt;X&gt;<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 生态里非常常用的自定义错误类型辅助库
帮你为自定义错误类型自动生成Displaystd::error::Errorsource()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
posted @ 2026-08-05 12:17  lxd670  阅读(11)  评论(0)    收藏  举报