Rust错误处理

问题背景

我们在Rust错误处理中常遇到的情况是,某个函数需要配合? 抛出多种错误

1
2
3
4
5
6
7
8
fn g1() -> Result<&'static str, std::io::Error> {
Ok("hello 1")
}

fn g2() -> Result<&'static str, std::net::AddrParseError> {
Ok("hello 2")
}

此时我们在main中想处理这几种错误怎么做呢?

1. trait object

一种常见的方式就是用trait object包装std::error::Error

1
2
3
4
5
fn main() -> Result<(), Box<dyn std::error::Error>> {
g1()?;
g2()?;
Ok(())
}

这种方式对于一些轻量场景工作的很好,比如外层只需要打印错误信息,并不关心其他细节。

1
2
3
4
5
6
7
8
9
10
11
fn main() -> Result<(), Box<dyn std::error::Error>> {
let r = handle();
println!("{:?}",r);
Ok(())
}

fn handle() -> Result<(), Box<dyn std::error::Error>> {
g1()?;
g2()?;
Ok(())
}

然而这种方式有几个问题:

  1. 如果我们想Error情况分别处理,那可能会遇到问题。

虽然trait object支持通过downcast做类型转换,但是必须预先知道错误类型。

1
2
3
4
5
6
7
8
9
10
fn main() -> Result<(), Box<dyn std::error::Error>> {
let r = handle();
match r {
Ok(_) => {},
Err(e) => {
let e0 = e.downcast::<std::io::Error>();
},
}
Ok(())
}
  1. 另一个麻烦的问题是,这种方式要求Result<T,E> 必须实现了std::error::Error,实际上 Result<T,E> 并没有限制E必须实现std::error::Error

2. 自己实现From

自己实现From,将多种Error转为一个Error枚举,? 会通过From进行隐式转换。

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
46
#[derive(Debug)]
enum MyAppError {
IO(std::io::Error),
Net(std::net::AddrParseError),
}

//
impl Display for MyAppError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "MyAppError:")?;
match self {
MyAppError::IO(e) => write!(f, "IO: {}", e)?,
MyAppError::Net(e) => write!(f, "Net: {}", e)?,
};
Ok(())
}
}

impl From<std::io::Error> for MyAppError {
fn from(e: std::io::Error) -> MyAppError {
MyAppError::IO(e)
}
}

impl From<std::net::AddrParseError> for MyAppError {
fn from(e: std::net::AddrParseError) -> MyAppError {
MyAppError::Net(e)
}
}

fn main() -> Result<(), MyAppError> {
if let Err(e) = handle() {
match e {
MyAppError::IO(e) => todo!(),
MyAppError::Net(e) => todo!(),
}
}
Ok(())
}

fn handle() -> Result<(), MyAppError> {
g1()?;
g2()?;
Ok(())
}

这种方式应该说失去了一些灵活性,也必须事先枚举出所有可能的错误。

3. thiserror

这个crates能简化一些上面的工作。

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
46
47

fn g1() -> Result<String, MyAppError> {
let bytes = vec![0, 159];
// 由于我们下面配置了 #[from] 所以这里可以用`?` 运算符
Ok(String::from_utf8(bytes)?)
}

fn g2() -> Result<&'static str, MyAppError> {
return Err(MyAppError::IO(std::io::Error::new(
std::io::ErrorKind::Other,
"test",
)));
}

#[derive(Debug, thiserror::Error)]
enum MyAppError {
#[error("MyApp IO Error {0} ")]
IO(#[from] std::io::Error),
#[error("MyApp FromUtf8Error Error {0}")]
Utf8(#[from] FromUtf8Error),
#[error("invalid header (expected {expected:?}, found {found:?})")]
InvalidHeader {
expected: String,
found: String,
},
}

fn main() -> Result<(), MyAppError> {
if let Err(e) = handle() {
// 只想打印错误
println!("{}",e);

// 想区分具体错误处理
match e {
MyAppError::IO(e) => println!("catch IO Error=> {}", e),
MyAppError::Utf8(e) => println!("catch Utf8 => {}", e)
}
}
Ok(())
}

fn handle() -> Result<(), MyAppError> {
g1()?;
g2()?;
Ok(())
}

在thiserror中 #[Error] 派生宏为struct 实现了,std::error::Error
#[error("MyApp IO Error {0} ")] 则为其实现了Display其中可以引用变量display。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#[allow(unused_qualifications)]
impl std::fmt::Display for MyAppError {
fn fmt(&self, __formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
#[allow(unused_imports)]
use thiserror::__private::{DisplayAsDisplay, PathAsDisplay};
#[allow(unused_variables, deprecated, clippy::used_underscore_binding)]
match self {
MyAppError::IO(_0) => write!(
__formatter,
"MyApp IO Error {field__0} ",
field__0 = _0.as_display()
),
MyAppError::Utf8(_0) => write!(
__formatter,
"MyApp FromUtf8Error Error {field__0}",
field__0 = _0.as_display()
),
}
}
}

#[from] 宏则实现了原错误类型到我们自定义的错误类型之间的转换。因此我们可以直接使用?

4. anyhow

所有实现了std::Error trait的结构体,统一转换成它定义的anyhow::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
use anyhow::Result;
use std::{fs::read_to_string};
use anyhow::Context;

fn g1() -> Result<String> {
let bytes = vec![0, 159];
Ok(String::from_utf8(bytes)?)
}

fn g2() -> Result<String> {
let file = std::env::var("MARKDOWN")?;
Ok(read_to_string(file)?)
}

fn main() -> Result<()> {
handle()?;
Ok(())
}

fn handle() -> Result<()> {
g1().with_context(||format!("fail to parse"))?;
g2()?;
Ok(())
}

with_context 是可以添加上下文。

1
2
3
4
5
6
7
8
9
10
11
Error: fail to 

Caused by:
invalid utf-8 sequence of 1 bytes from index 1

Stack backtrace:
0: std::backtrace_rs::backtrace::libunwind::trace
at /rustc/b8b5caee04116c7383eb1c6470fcf15c437a60d4/library/std/src/../../backtrace/src/backtrace/mod.rs:66:5
1: std::backtrace_rs::backtrace::trace_unsynchronized
at /rustc/b8b5caee04116c7383eb1c6470fcf15c437a60d4/library/std/src/../../backtrace/src/backtrace/mod.rs:66:5

可以基于字符串生成anyhow::Error

1
2
3
4
fn g2() -> Result<String> {
let file = std::env::var("MARKDOWN")?;
Err(anyhow::anyhow!("fail to read file"))
}

实践经验表明:在库开发中不应使用anyhow,这是因为anyhow暴露出的错误仍抹去了原信息,对于希望获取原Error的用户则需要使用downcast转换,不是很好的设计。库开发使用thiserror明确提供enum是比较好的设计。
在应用开发中则可以使用anyhow和thiserror结合。