Rust自定义错误生成方案:thiserror

总结摘要
本文介绍了 Rust 中的 thiserror 库,用于简化自定义错误类型的创建。thiserror 提供了一系列派生宏,帮助开发者快速实现 std::error::Error trait,支持错误信息格式化、从其他错误生成、错误源获取、错误回溯等功能。通过 #[error] 属性,可以自定义错误消息,并支持字段值的动态插入。#[from] 和 #[source] 属性分别用于自动实现 From trait 和获取底层错误类型。此外,#[error(transparent)] 可以让错误直接使用底层错误的 Display 和 source 方法。这些功能使得 thiserror 成为 Rust 中处理错误的强大工具。

对于程序开发者而言,我们常常与错误处理代码打交道。当我们需要创建自定义错误类型时,常常需要用到thiserror库,可以帮助我们极大的简化代码。

thiserror库为std::error::Errortrait 提供了一系列方便的派生宏,可以帮助我们简化 Rust 中自定义错误的创建与处理。

快速使用

首先我们需要在我们的项目中引入thiserror库。

命令行方式添加:

1
cargo add serde

你也可以直接编辑Cargo.toml文件,并添加以下内容:

1
2
[dependencies]
thiserror = "1.0"

自定义错误示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
use std::io;
use thiserror::Error;

#[derive(Error, Debug)]
pub enum DataStoreError {
    #[error("data store disconnected")]
    Disconnect(#[from] io::Error),
    #[error("the data for key `{0}` is not available")]
    Redaction(String),
    #[error("invalid header (expected {expected:?}, found {found:?})")]
    InvalidHeader { expected: String, found: String },
    #[error("unknown data store error")]
    Unknown,
}

详细用法

错误信息格式化

首先自定义的错误可以是一个枚举体、具名结构体、元组结构体、单元结构体。

如果您在结构体或者枚举体的各个变体上面添加了#[error("...")]信息,那么会自动为该错误生成Displaytrait 实现。如下面示例所示:

这些消息支持从错误中插入相应的字段值:

  • #[error("{var}")] ⟶ write!("{}", self.var)
  • #[error("{0}")] ⟶ write!("{}", self.0)
  • #[error("{var:?}")] ⟶ write!("{:?}", self.var)
  • #[error("{0:?}")] ⟶ write!("{:?}", self.0)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
use thiserror::Error;

// 枚举体
#[derive(Error, Debug)]
pub enum MyError1 {
    #[error("unknown data store error")]
    Unknown,
}

// 具名结构体
#[derive(Debug, Error)]
#[error("MyError2: code={code}, message={message}")]
pub struct MyError2 {
    code: i32,
    message: String,
}

// 元组结构体
#[derive(Error, Debug)]
#[error("MyError3: code={0}, message={1}")]
pub struct MyError3(i32, String);

// 单元结构体
#[derive(Error, Debug)]
#[error("MyError4")]
pub struct MyError4;

fn main() {
    let e1 = MyError1::Unknown;
    let e2 = MyError2 {
        code: 100,
        message: "error message".to_string(),
    };
    let e3 = MyError3(200, "error message".to_string());
    let e4 = MyError4;

    // 输出:unknown data store error
    println!("{}", e1);
    // 输出:MyError2: code=100, message=error message
    println!("{}", e2);
    // 输出:MyError3: code=200, message=error message
    println!("{}", e3);
    // 输出:MyError4
    println!("{}", e4);
}

这些插入值的插槽也可以配合任何其他格式化参数一起使用,其他参数可能是任意表达式,例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
use thiserror::Error;

#[derive(Error, Debug)]
pub enum Error {
    #[error("invalid rdo_lookahead_frames {0} (expected < {})", i32::MAX)]
    InvalidLookahead(u32),
}

fn main() {
    let err = Error::InvalidLookahead(42);
    // 输出:invalid rdo_lookahead_frames 42 (expected < 2147483647)
    println!("{}", err);
}

如果附加表达式参数之一需要引用结构体或枚举体的字段,则将命名字段引用为.var,如果是引用元组字段可以用.0

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
use thiserror::Error;

#[derive(Debug)]
pub struct Limits {
    lo: usize,
    hi: usize,
}

fn first_char(s: &str) -> char {
    s.chars().next().unwrap()
}

#[derive(Error, Debug)]
pub enum Error {
    #[error("first letter must be lowercase but was {:?}", first_char(.0))]
    WrongCase(String),
    #[error("invalid index {idx}, expected at least {} and at most {}", .limits.lo, .limits.hi)]
    OutOfBounds { idx: usize, limits: Limits },
}

fn main() {
    let error1 = Error::WrongCase("Hello".to_string());
    let error2 = Error::OutOfBounds {
        idx: 5,
        limits: Limits { lo: 0, hi: 4 },
    };
    // 输出:first letter must be lowercase but was 'H'
    println!("{}", error1);
    // 输出:invalid index 5, expected at least 0 and at most 4
    println!("{}", error2);
}

#[from]从其他错误生成

自动为每个包含#[from]属性的变体自动实现Fromtrait。

请注意,变体不得包含源错误之外的任何字段,也可能包含错误回溯。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
use std::{backtrace::Backtrace, io};
use thiserror::Error;

#[derive(Error, Debug)]
pub enum MyError {
    #[error("some io error happened, {:?}", .source)]
    Io {
        #[from]
        source: io::Error,
        backtrace: Backtrace,
    },
}

#[source]源错误获取

可以使用 #[source] 属性,或者将字段命名为 source,可为自定义错误实现 source 方法,返回底层的错误类型:

#[from]属性始终意味着同一字段是#[source],所以你不需要同时指定这两个属性,两者二选一即可。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
use std::error::Error;
use std::io;
use thiserror::Error;

#[derive(Error, Debug)]
pub enum MyError {
    #[error("some io error happened, {:?}", .err)]
    IO {
        #[source]
        err: io::Error,
    },
}

fn main() {
    let err = MyError::IO {
        err: io::Error::from(io::ErrorKind::TimedOut),
    };
    // 输出:Some(Kind(TimedOut))
    println!("{:?}", err.source());
}

错误回溯

如果存在一个类型名为Backtrace的字段,就会自动实现Errortrait的provide()方法。

1
2
3
4
5
6
7
use std::backtrace::Backtrace;

#[derive(Error, Debug)]
pub struct MyError {
    msg: String,
    backtrace: Backtrace,  // automatically detected
}

如果一个字段既是一个source(字段名为source,或者有#[source]#[from]标记),并且被标记了#[backtrace],那么Errortrait的provide()方法会转发到源错误,以便两层错误能够共享相同的错误回溯信息。

1
2
3
4
5
6
7
#[derive(Error, Debug)]
pub enum MyError {
    Io {
        #[backtrace]
        source: io::Error,
    },
}

#[error(transparent)]

可以通过 #[error(transparent)]sourceDisplay 直接使用底层的错误,这对于那些想处理任何的枚举来说是很有用的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
use std::io;
use thiserror::Error;

#[derive(Error, Debug)]
pub enum MyError {
    #[error("file not found")]
    FileNotFound,
    #[error(transparent)]
    Other(#[from] io::Error), // source and Display delegate to anyhow::Error
}

fn main() {
    let err = MyError::from(io::Error::from(io::ErrorKind::TimedOut));
    // 输出: timed out
    println!("{}", err);
}