Rust进阶必备:thiserror用法全面解析

引言

这是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可以较好的解决这些问题,下面先入个门。

一、简单入门

跟之前一样,先发个入门示例,复制粘贴就可以运行,方便理解。

  • 项目结构

    css 复制代码
    thiserror-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"
  • main.rs

    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:

    rust 复制代码
    impl From<ParseIntError> for AppError {
        fn from(err: ParseIntError) -> Self {
            AppError::Parse(err)
        }
    }
  • parse 返回 ParseIntError后走到AppError::Parse(err),实现链式调用。thiserror 的 #[from] 会自动生成 From impl,无需手动写。

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 错误。
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等库一起用。

如果觉得本文有用,请点个关注吧,本人公众号大鱼七成饱

相关推荐
Amos_Web4 小时前
Rust实战(四):数据持久化、告警配置与Web API —— 构建监控系统的功能闭环
前端·后端·rust
联系QQ:4877392785 小时前
Bayes-CNN-LSTM、Bayes-CNN-BiLSTM、Bayes-CNN-GRU、B...
rust
空白诗11 小时前
tokei 在鸿蒙PC上的构建与适配
后端·华为·rust·harmonyos
疏狂难除12 小时前
尝试rust与python的混合编程(一)
开发语言·后端·python·rust
H***997618 小时前
Rust在WebAssembly中的使用
开发语言·rust·wasm
百锦再19 小时前
[特殊字符] HBuilder uni-app UI 组件库全方位对比
android·java·开发语言·ui·rust·uni-app·go
q***d1731 天前
Rust并发模型
开发语言·后端·rust
q***48311 天前
数据库操作与数据管理——Rust 与 SQLite 的集成
数据库·rust·sqlite
Andrew_Ryan1 天前
达梦 数据库 Rust 实战
数据库·rust·数据分析