【Rust】anyhow & thiserror

Rust 中使用 std::result::Result 表示可能出错的操作,成功的时候是 Ok(T),而出错的时候则是 Err(E)

1
2
3
4
pub enum Result<T, E> {
Ok(T),
Err(E),
}

通常情况下,E 是实现 std::error::Error 的错误类型:

1
2
3
4
5
6
pub trait Error: Debug + Display {
fn source(&self) -> Option<&(dyn Error + 'static)> { ... }
fn backtrace(&self) -> Option<&Backtrace> { ... }
fn description(&self) -> &str { ... }
fn cause(&self) -> Option<&dyn Error> { ... }
}

我们通常也需要在自己的代码中自定义错误,并且为之手动实现 std::error::Error,这个工作很麻烦,所以就有了 thiserror,自动帮我们生成实现的 std::error::Error 的代码。

而借助于 anyhow::Error,和与之对应的 Result<T, anyhow::Error>,等价于 anyhow::Result<T>,我们可以使用 ? 在可能失败的函数中传播任何实现了 std::error::Error 的错误。

thiserror

可以使用命令 cargo add thiserror 将它添加到自己的项目中,或者在 Cargo.toml 中添加如下的配置:

1
2
[dependencies]
thiserror = "1.0"

thiserror 可以用于枚举或者结构体,例如,我们来看一个基本的例子:

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

#[derive(Error, Debug)]
pub enum DataStoreError {
#[error("data store disconnected")]
Disconnect(#[from] std::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]

如果使用 #[error(...)] 为结构体或者枚举生成自定义错误消息,这将为它们实现 Display

1
2
3
4
5
6
7
use thiserror::Error;

#[derive(Error, Debug)]
#[error("something failed, msg is: {msg}")]
pub struct MyError {
msg: &'static str,
}

我们可以在错误中插入字段的简写,一共有四种形式:

  1. #[error("{var}")] <=> write!("{}", self.var)
  2. #[error("{0}")]  <=> write!("{}", self.0)
  3. #[error("{var:?}")] <=> write!("{:?}", self.var)
  4. #[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
use thiserror::Error;

pub fn first_char(s: &String) -> char {
if s.len() == 0 {
'-'
} else {
s.chars().next().unwrap_or('-')
}
}

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

#[derive(Error, Debug)]
pub enum Error {
#[error("invalid rdo_lookahead_frames {0} (expected < {})", i32::MAX)]
InvalidLookahead(u32),
#[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 },
}

#[from]

可以使用 #[from] 注解为错误类型实现 From,可以从其他错误生成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#![allow(unused)]
#![feature(backtrace)]

use std::backtrace;
use std::error::Error as _;
use std::io;
use thiserror::Error;

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

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

#[source]

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

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

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

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

或者使用 #[source] 属性标记非 source 的字段,例如:这里是 err 字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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),
};
println!("{:?}", err.source());
}

#[from]#[source] 二选一即可,#[from] 也会为类型生成 .source() 方法,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#![allow(unused)]
#![feature(backtrace)]

use std::backtrace;
use std::error::Error as _;
use std::io;
use thiserror::Error;

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

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

#[backtrace]

只要在我们的错误结构体里面放个类型为 std::backtrace::Backtrace 的字段,就会自动实现 backtrace() 方法,可以看 #[from]

另外,如果使用 #[backtrace] 标记 sourcesource字段,或者 #[source],或者 #[from]),那么 backtrace() 方法会转发到 sourcebacktrace

文档里面的例子(没理解,以后再来改):

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
17
18
#![allow(unused)]

use anyhow::anyhow;
use std::error::Error as _;
use std::io;
use thiserror::Error;

#[derive(Error, Debug)]
#[error(transparent)]
pub struct MyError {
#[from]
source: anyhow::Error,
}

fn main() {
let err = MyError::from(anyhow!("Missing attribute: {}", "field1"));
println!("{:?}", err);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#![allow(unused)]

use anyhow::anyhow;
use std::error::{self, Error as _};
use std::io;
use thiserror::Error;

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

fn main() {
let err = MyError::from(anyhow!("Missing attribute: {}", "field1"));
println!("{:?}", err);
}

anyhow

anyhow::Error 是这个 crate 中最重要的结构体,它是动态错误类型的包装器,能从所有实现了 std::error::Error + Send + Sync + 'static 的错误转换而来,也能转换成 Box<dyn std::error::Error + Send + Sync + 'static>,它有以下特点:

  1. anyhow::Error 要求包裹的错误必须是 Send + Sync + 'static
  2. anyhow::Error 保证 backtrace 是可用的,就是底层的错误类型没有提供;
  3. anyhow::Error 在内存中只占一个机器字而不是两个;

如果我们要将 anyhow::Error 以文本形式展出来,可以有下面几种形式:

  1. 可以使用 {} 或者 .to_string(),但是仅仅打印最外层错误或者上下文,而不是内层的错误;

  2. 可以使用 {:#} 打印外层和底层错误;

  3. 可以使用 {:?} 在调试模式打印错误以及调用栈;

  4. 可以使用 {:#?} 以结构体样式打印错误,例如:

    1
    2
    3
    4
    5
    6
    7
    8
    Error {
    context: "Failed to read instrs from ./path/to/instrs.json",
    source: Os {
    code: 2,
    kind: NotFound,
    message: "No such file or directory",
    },
    }

另外,既然 anyhow::Error 包装了底层的错误,那就得提供找到内层错误的方法,这里是 downcast_ref

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
#![allow(unused)]

use anyhow::{anyhow, bail};
use std::io;
use thiserror::Error;

#[derive(Error, Debug)]
pub enum DataStoreError {
#[error("the data for key `{0}` is not available")]
Redaction(String),
}

fn foo() -> anyhow::Result<()> {
// 使用 ?运算符能将任何实现了 std::error::Error + Send + Sync + 'static 的错误转换为 anyhow::Error
std::fs::read_to_string("config.json")?;
Ok(())
}

fn main() {
match foo() {
Ok(()) => (),
Err(ref root_cause) => {
let err = root_cause.downcast_ref::<DataStoreError>();
match err {
Some(DataStoreError::Redaction(_)) => (),
None => (),
}
println!("{:#?}", root_cause);
}
}
}

anyhow!

使用 anyhow! 这个宏可以生成 anyhow::Error类型的值,它可以接受字符串,格式化字符串作为参数,或者实现 std::error:Error 的错误作为参数。

1
2
3
4
5
6
7
8
9
use anyhow::{anyhow, Result};

fn lookup(key: &str) -> Result<V> {
if key.len() != 16 {
return Err(anyhow!("key length must be 16 characters, got {:?}", key));
}

// ...
}

或者从实现了 std::error::Error 的错误转换而来:

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
use anyhow::anyhow;
use thiserror::Error;

#[derive(Error, Debug)]
pub enum DataStoreError {
#[error("the data for key `{0}` is not available")]
Redaction(String),
}

fn bar() -> std::result::Result<(), DataStoreError> {
Err(DataStoreError::Redaction("".to_owned()))
}

fn foo1() -> anyhow::Result<()> {
let a = bar()?;
Ok(())
}

fn foo2() -> anyhow::Result<()> {
Err(anyhow::Error::from(DataStoreError::Redaction(
"".to_string(),
)))
}

fn foo3() -> anyhow::Result<()> {
Err(anyhow!(DataStoreError::Redaction("".to_owned())))
}

fn main() {}

又或者:

1
2
3
4
5
use anyhow::anyhow;

fn foo() -> anyhow::Result<()> {
Err(anyhow!("missing {} field", "f1"))
}

bail!

anyhow::bail 宏用于提前错误返回,它等价于 return Err(anyhow!($args...)),包含这个宏的函数的返回值必须是 Result<_,anyhow::Error>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
use anyhow::{anyhow, bail};
use thiserror::Error;

#[derive(Error, Debug)]
pub enum DataStoreError {
#[error("the data for key `{0}` is not available")]
Redaction(String),
}

fn foo(i: i16) -> anyhow::Result<()> {
if i < 0 {
bail!(DataStoreError::Redaction("something wrong".to_string()));
}
Ok(())
}

anyhow::Context

anyhow::Contextanyhow::Result 类型提供了 context 方法,能在错误发生时提供更多的上下文信息:

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
use anyhow::{anyhow, Context, Result};
use std::fs;
use std::path::PathBuf;

pub struct ImportantThing {
path: PathBuf,
}

impl ImportantThing {
pub fn detach(&mut self) -> Result<()> {
Err(anyhow!("detach faield"))
}
}

pub fn do_it(mut it: ImportantThing) -> Result<Vec<u8>> {
it.detach()
.context("Failed to detach the important thing")?;

let path = &it.path;
let content =
fs::read(path).with_context(|| format!("Failed to read instrs from {}", path.display()))?;

Ok(content)
}

fn main() {
let mut it = ImportantThing {
path: PathBuf::new(),
};
match do_it(it) {
Ok(_) => (),
Err(ref err) => {
for cause in err.chain() {
println!("{}", cause);
}
}
}
}

这段代码将输出:

Failed to detach the important thing
detach faield

对于下面的代码也是输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pub fn do_it(it: &mut ImportantThing) -> Result<Vec<u8>> {
let path = &it.path;
let content =
fs::read(path).with_context(|| format!("Failed to read instrs from {}", path.display()))?;

Ok(content)
}

fn main() {
let mut it = ImportantThing {
path: PathBuf::new(),
};
match do_it(&mut it) {
Ok(_) => (),
Err(ref err) => {
for cause in err.chain() {
println!("{}", cause);
}
}
}
}

将输出:

Failed to read instrs from 
No such file or directory (os error 2)