自定义错误和error_chain

1 前言

上一篇文章聊到 Rust 的错误处理机制,以及和 Java 的简单比较,现在就来聊一下如何在 Rust 自定义错误,以及引入 error_chain这个库来优雅地进行错误处理。

还有,少不了用 Java 来做对比咯:)

1.1 Java 自定义异常

前文简单提到 Java 的错误和异常但是继承自一个 Throwable的父类,既然异常是继承自异常父类的,我们自定义异常的时候, 也可以模仿JDK, 继承一个异常类:

1
2
3
4
5
public class MyException extends Exception {
    public MyException(String message) {
	super(message);
    }
}

这样就定义了属于自己的异常. 只需要继承 Exception,然后调用父类的构造方法。

不过 对于那些复杂的项目,这样的例子未免过于简单。现在就来看一个我项目的中的一个异常类:

 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
public final class MyError extends RuntimeException {
    /**
     *
     */
    private static final long serialVersionUID = 1L;

    private static boolean isFillStack = true;

    private Integer httpStatusCode;
    private Integer code;
    private String message;

    private MyError(int httpStatusCode, int code, String message) {
	super(message, null, isFillStack, isFillStack);
	this.httpStatusCode = httpStatusCode;
	this.code = code;
	this.message = message;
    }

    public MyError(MyErrorCode myErrorCode, Object... messageArgs) {
	this(myErrorCode.getHttpStatusCode(), myErrorCode.getErrorCode(),
	     MessageFormat.format(myErrorCode.getMessagePattern(), messageArgs));
    }

    public static MyError throwError(MyErrorCode myErrorCode, Object... messageArgs) {
	throw new MyError(myErrorCode, messageArgs);
    }

    public static MyError internalServerError(String logId) {
	throw new MyError(MyErrorCode.INTERNAL_SERVER_ERROR, logId);
    }
    public static MyError DataError(String logId) {
	throw new MyError(MyErrorCode.DATA_ERROR, logId);
    }

    public static MyError BadParameterError(String logId) {
	throw new MyError(MyErrorCode.BAD_PARAMETER_ERROR, logId);
    }
}

这是我去掉了多余方法和变量的简化版,但是也足以一叶知秋了。

MyError这个异常类是 继承于 RuntimeException的,并调用了 RuntimeException的构造方法。

因为我的项目是 WEB 服务的业务层,要处理大量的逻辑,难免会出现异常.

比如说可能调用方调用接口 的时候,入参不符合规范,我就抛出一个经过包装的 BadParameterError 异常,对于接 口调用方,这样会比一个单纯的 400 错误要友好,其他的异常也是同理。

1.2 Rust 自定义错误

对于习惯了 OOP 编程的同学来说,Java 的异常是很容易理解,但是回到 Rust 身上,Rust是没有父类一说的,显然,Rust 是没可能套用 Java 的自定义异常的方式的。

Rust 用的是 trait, trait就有点类似 Java 的 =interface=(只是类似,不是等同!).

按照 Rust 的规范,Rust 允许开发者定义自己的错误,设计良好的错误应该包含以下的特性:

  1. 使用相同的类型(type)来表示不同的错误
  2. 错误中包含对用户友好的提示(我也在上面提到的)
  3. 能便捷地与其他类型比较,例如:
    • Good: Err(EmptyVec)
    • Bad: Err("Please use a vector with at least one element".to_owned())
  4. 包含与错误相关的信息,例如:
    • Good: Err(BadChar(c, position))
    • Bad: Err("+ cannot be used here".to_owned())
  5. 可以很方便地与其他错误结合
 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
48
49
50
51
52
53
54
55
56
use std::error;
use std::fmt;
use std::num::ParseIntError;

type Result<T> = std::result::Result<T, DoubleError>;

#[derive(Debug, Clone)]
// 自定义错误类型。
struct DoubleError;

// 不同的错误需要展示的信息也不一样,这个就要视情况而定,因为 DoubleError 没有定义额外的字段来保存错误信息
// 所以就现在就简单打印错误信息
impl fmt::Display for DoubleError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
	write!(f, "invalid first item to double")
    }
}

// 实现 error::Error 这个 trait, 对DoubleError 进行包装
impl error::Error for DoubleError {
    fn description(&self) -> &str {
	"invalid first item to double"
    }

    fn cause(&self) -> Option<&error::Error> {
	// Generic error, underlying cause isn't tracked.
	None
    }
}

fn double_first(vec: Vec<&str>) -> Result<i32> {
    vec.first()
    // Change the error to our new type.
	.ok_or(DoubleError)
	.and_then(|s| s.parse::<i32>()
		  // Update to the new error type here also.
		  .map_err(|_| DoubleError)
		  .map(|i| 2 * i))
}

fn print(result: Result<i32>) {
    match result {
	Ok(n)  => println!("The first doubled is {}", n),
	Err(e) => println!("Error: {}", e),
    }
}

fn main() {
    let numbers = vec!["42", "93", "18"];
    let empty = vec![];
    let strings = vec!["tofu", "93", "18"];

    print(double_first(numbers));
    print(double_first(empty));
    print(double_first(strings));
}

这段代码的运行结果如下:

1
2
3
The first doubled is 84
Error: invalid first item to double
Error: invalid first item to double

1.3 error_chain

虽说 Rust 自定义错误很灵活和方便,但是如果每次定义异常都需要实现 DisplayError, 未免过于繁琐,现在来介绍 error_chain 这个类库。

error_chain 是由 Rust 项目组的 leader–Brian Anderson 编写的异常处理库,可以让你更舒心简单不粗 暴地定义错误。


1.3.1 error_chain示例

以上面的 DoubleError为例,并改写 error_chain 的官方例子 以实现相同的效果,代码如下:

 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
// `error_chain!` 的递归深度
#![recursion_limit = "1024"]

//引出 error_chain 和相应的宏
#[macro_use]
extern crate error_chain;

//将跟错误有关的内容放入 errors module, 其他需要用到这个错误module 的模块就通过
// use errors::* 来引入所有内容
mod errors {
    // Create the Error, ErrorKind, ResultExt, and Result types
    error_chain! {
	errors{Double{
	    description("invalid first item to double")
		display("invalid first item to double")
	}}
    }
}

use errors::*;
pub type Result<T> = ::std::result::Result<T, ErrorKind>;
fn main() {
    let numbers = vec!["42", "93", "18"];
    let empty = vec![];
    let strings = vec!["tofu", "93", "18"];

    print(double_first(numbers));
    print(double_first(empty));
    print(double_first(strings));

}
fn double_first(vec: Vec<&str>) -> Result<i32> {
    vec.first()
    // Change the error to our new type.
	.ok_or(ErrorKind::Double)
	.and_then(|s| s.parse::<i32>()
		  // Update to the new error type here also.
		  .map_err(|_| ErrorKind::Double)
		  .map(|i| 2 * i))
}

fn print(result: Result<i32>) {
    match result {
	Ok(n) => println!("The first doubled is {}", n),
	Err(e) => println!("Error: {}", e),
    }
}

运行这代码可以得到和上面小节同样的输出。

1.3.2 error_chain 详解

刚刚就先目睹了一下 error_chain 的芳容了,现在是时候来解剖一下 error_chain, 这次就以 error_chainexample来解释

 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
48
49
50
51
// Simple and robust error handling with error-chain!
// Use this as a template for new projects.

// `error_chain!` can recurse deeply
#![recursion_limit = "1024"]

// Import the macro. Don't forget to add `error-chain` in your
// `Cargo.toml`!
#[macro_use]
extern crate error_chain;

// We'll put our errors in an `errors` module, and other modules in
// this crate will `use errors::*;` to get access to everything
// `error_chain!` creates.
mod errors {
    // Create the Error, ErrorKind, ResultExt, and Result types
    error_chain! { }
}

use errors::*;

fn main() {
    if let Err(ref e) = run() {
	println!("error: {}", e);

	for e in e.iter().skip(1) {
	    println!("caused by: {}", e);
	}

	// The backtrace is not always generated. Try to run this example
	// with `RUST_BACKTRACE=1`.
	if let Some(backtrace) = e.backtrace() {
	    println!("backtrace: {:?}", backtrace);
	}

	::std::process::exit(1);
    }
}

// Most functions will return the `Result` type, imported from the
// `errors` module. It is a typedef of the standard `Result` type
// for which the error type is always our own `Error`.
fn run() -> Result<()> {
    use std::fs::File;

    // This operation will fail
    File::open("contacts")
	.chain_err(|| "unable to open contacts file")?;

    Ok(())
}

重要的信息例子已经作了注释,现在就来看看用法。首先来看看 main 函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
fn main() {
    if let Err(ref e) = run() {
	println!("error: {}", e);

	for e in e.iter().skip(1) {
	    println!("caused by: {}", e);
	}

	// The backtrace is not always generated. Try to run this example
	// with `RUST_BACKTRACE=1`.
	if let Some(backtrace) = e.backtrace() {
	    println!("backtrace: {:?}", backtrace);
	}

	::std::process::exit(1);
    }
}

可以看出,这个函数的大部份逻辑是进行错误处理,例如返回自定义的 ResultError, 然后处理这些错误。上面的处理流程显示了 error_chain 从某个错误继承而来的三样信息:最近出现的错误(即e),导致错误的调用链,原来错误的堆栈信息 (e.backtrace())

2 小结

刚刚的例子只是 error_chain 小试了一波牛刀,如果想要了解更多关于 Rust 异常处理 的细节,就需要看看 Rust 的文档咯

3 参考