# Chapter 7 - Error Handling

Rust的可靠性之一就在于它的错误处理。在很多情况下,Rust要求你在代码编译之前对所有可能的错误情况进行处理。在部署到生产环境之前确保发现并处理错误,这让你的代码健壮性更强。

Rust将错误分为两个大类:recoverableunrecoverable。前者例如读取不存在的文件,将该错误上报并且重试这个操作是合理的行为。后者一半是由于代码逻辑bug,例如数组越界访问。

大多数语言不会对这两个大类进行区分,一般都是采用同一套解决方式,例如异常机制。Rust没有异常机制,Rust用Result<U,E>处理recoverable类型的错误,用panic!宏处理unrecoverable类型的错误。

# Section 1 - Unrecoverable Errors with panic!

当出现一些意料之外的错误,并且没有后续的处理逻辑或者不知道该如何处理时,Rust有panic!宏。当panic!宏调用时,打印出错误信息,然后释放清空堆栈内存退出程序。

Unwinding the Stack or Aborting in Response to a Panic

一般来说,当panic发生后,Rust会进入unwinding阶段,它需要到堆栈顶部,开始遍历堆栈清空数据和函数,这是一个很费时的操作。另一种方案是abort,即程序退出时Rust不会清空堆栈,而将这个操作交给操作系统。如果你需要你的项目编译结果尽可能小,你可以通过设置Cargo.toml文件来让你的程序为abort模式。

[profile.release]
panic = 'abort'
1
2

下面简单调用一下panic!宏来看一看它的输出。

fn main() {
    panic!("crash");
}
// thread 'main' panicked at 'crash', src/main.rs:17:9
// note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

1
2
3
4
5
6

可以看到panic!输出了两行信息,第一行是错误信息和错误在源代码在文件中的位置。

在这个例子中,我们可以跟踪代码在对应的位置找到导致panic!宏调用的代码。

# Using a panic! Backtrace

我们可以设置RUST_BACKTRACE环境变量来获取发生错误的完整调用链路。它是一个函数的调用堆栈列表,从栈顶开始一直到我们自己的代码文件。这个链路中可能包含核心文件、标准库文件、其他你使用到的第三方模块代码。你所在文件那一行上面的内容是你的代码调用的文件,下面的内容是调用你代码的文件。

stack backtrace:
    0: backtrace::backtrace::libunwind::trace
                at /Users/runner/.cargo/registry/src/github.com-1ecc6299db9ec823/backtrace-0.3.46/src/backtrace/libunwind.rs:86
    1: backtrace::backtrace::trace_unsynchronized
                at /Users/runner/.cargo/registry/src/github.com-1ecc6299db9ec823/backtrace-0.3.46/src/backtrace/mod.rs:66
    2: std::sys_common::backtrace::_print_fmt
                at src/libstd/sys_common/backtrace.rs:78
    3: <std::sys_common::backtrace::_print::DisplayBacktrace as core::fmt::Display>::fmt
                at src/libstd/sys_common/backtrace.rs:59
    4: core::fmt::write
                at src/libcore/fmt/mod.rs:1076
    5: std::io::Write::write_fmt
                at src/libstd/io/mod.rs:1537
    6: std::sys_common::backtrace::_print
                at src/libstd/sys_common/backtrace.rs:62
    7: std::sys_common::backtrace::print
                at src/libstd/sys_common/backtrace.rs:49
    8: std::panicking::default_hook::
                at src/libstd/panicking.rs:198
    9: std::panicking::default_hook
                at src/libstd/panicking.rs:218
    10: std::panicking::rust_panic_with_hook
                at src/libstd/panicking.rs:486
    11: rust_begin_unwind
                at src/libstd/panicking.rs:388
    12: core::panicking::panic_fmt
                at src/libcore/panicking.rs:101
    13: core::panicking::panic_bounds_check
                at src/libcore/panicking.rs:73
    14: <usize as core::slice::SliceIndex<[T]>>::index
                at /Users/ksleo/.rustup/toolchains/stable-x86_64-apple-darwin/lib/rustlib/src/rust/src/libcore/slice/mod.rs:2872
    15: core::slice::<impl core::ops::index::Index<I> for [T]>::index
                at /Users/ksleo/.rustup/toolchains/stable-x86_64-apple-darwin/lib/rustlib/src/rust/src/libcore/slice/mod.rs:2732
    16: <alloc::vec::Vec<T> as core::ops::index::Index<I>>::index
                at /Users/ksleo/.rustup/toolchains/stable-x86_64-apple-darwin/lib/rustlib/src/rust/src/liballoc/vec.rs:1942
    17: p::main
                at src/main.rs:5
    18: std::rt::lang_start::
                at /Users/ksleo/.rustup/toolchains/stable-x86_64-apple-darwin/lib/rustlib/src/rust/src/libstd/rt.rs:67
    19: std::rt::lang_start_internal::
                at src/libstd/rt.rs:52
    20: std::panicking::try::do_call
                at src/libstd/panicking.rs:297
    21: std::panicking::try
                at src/libstd/panicking.rs:274
    22: std::panic::catch_unwind
                at src/libstd/panic.rs:394
    23: std::rt::lang_start_internal
                at src/libstd/rt.rs:51
    24: std::rt::lang_start
                at /Users/ksleo/.rustup/toolchains/stable-x86_64-apple-darwin/lib/rustlib/src/rust/src/libstd/rt.rs:67
    25: main

为了获取这个输出,debug标识必须是enable的,在运行cargo build或者cargo run的并且不带--release选项的时候,该标识默认是enable的。具体的输出内容和你的操作系统以及Rust版本有关。

# Section 2 - Recoverable Errors with Result

大多数错误抛出的时候,都没有必要将程序退出。比如,当读取的文件不存在时,可以考虑创建该文件而不是终止进程。

Result枚举有两个值,OkErr

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

T和E是泛型变量。T代表成功情况下返回值的类型,E代表失败情况下错误的返回类型。

use std::fs::File;

fn main() {
    let f = File::open("hello.txt");
}
1
2
3
4
5

如何知道File::open函数返回的是一个Result枚举呢?一种方式是查看标准库API文档,另外一种方式是给变量f指定一个其他的数据类型。然后编译代码,编译器会给出类型不匹配的错误信息。

这里泛型变量T会被填充为成功值的类型,在这里是一个std::fs::File类型的文件句柄,E则是std::io::Error类型。这意味着File::open函数可能会返回一个文件句柄,可以用来进行读写。或者可能返回一个io错误。

因此我们需要用match表达式对Result的所有情况进行覆盖。

use std::fs::File;

fn main() {
    let f = File::open("hello.txt");

    let f = match f {
        Ok(file) => file,
        Err(error) => panic!("Problem opening the file: {:?}", error),
    };
}
1
2
3
4
5
6
7
8
9
10

# Matching on Different Errors

上面的代码,当打开文件出错时,不论何种错误都会调用panic!宏然后退出程序。而我们的期望时根据不同的错误类型,有不同的处理方案。比如因为文件不存在,我们希望创建文件;如果是因为没有权限,则调用panic!宏退出程序。

use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let f = File::open("hello.txt");

    let f = match f {
        Ok(file) => file,
        Err(error) => match error.kind() {
            ErrorKind::NotFound => match File::create("hello.txt") {
                Ok(fc) => fc,
                Err(e) => panic!("Problem creating the file: {:?}", e),
            },
            other_error => {
                panic!("Problem opening the file: {:?}", other_error)
            }
        },
    };
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

File::open函数返回的是一个标准库提供的io::Error类型的错误。这个类型上有一个kind方法用来获取io::ErrorKind类型的值。这个类型也由标准库提供,枚举了一些io操作可能出现的错误类型。我们想要在ErrorKind::NotFound错误类型出现时,创建一个新文件。由于File::create方法也有可能失败,所以也需要用match表达式覆盖可能出现的情况。

但是这里出现了太多的match表达式嵌套。后面会介绍*闭包(closure)*的用法。

# Shortcuts for Panic on Error: unwrap and expect

match表达式能够满足需求。但是太多的match显得太啰嗦,表意也不够清晰。Result<T, E>上有许多工具函数,其中一个叫做unwrap的函数可以作为match表达式的语法糖使用。如果是成功状态,unwrap方法会返回值;如果是失败状态,unwrap会调用panic!宏。

use std::fs::File;

fn main() {
    let f = File::open("hello.txt").unwrap();
}
1
2
3
4
5

另外一个expect方法,作用和unwrap一样,但是可以让我们指定错误输出信息。可以表达我们想表达的错误信息,在错误追踪时也比较容易。

use std::fs::File;

fn main() {
    let f = File::open("hello.txt").expect("Failed to open hello.txt");
}
1
2
3
4
5

# Propagating Errors

当你实现一个函数时,它的实现可能会抛出某些错误,与其在你的函数中捕获这个错误,不如把这个错误传递给调用者,好让调用者决定如何处理这个错误。这个被称为错误的传递(propgation),这给了调用者更多的控制权,它内部也许有更完善的信息和逻辑来处理错误。

例如,我们要写一个函数,在一个文件中读取一些内容,如果读取错误,将这个错误抛给调用者。

use std::fs::File;
use std::io;
use std::io::Read;

fn read_username_from_file() -> Result<String, io::Error> {
    let f = File::open("hello.txt");

    let mut f = match f {
        Ok(file) => file,
        Err(e) => return Err(e),
    };

    let mut s = String::new();

    match f.read_to_string(&mut s) {
        Ok(_) => Ok(s),
        Err(e) => Err(e),
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# A Shortcut for Propagating Errors: the ? Operator

use std::fs::File;
use std::io;
use std::io::Read;

fn read_username_from_file() -> Result<String, io::Error> {
    let mut f = File::open("hello.txt")?;
    let mut s = String::new();
    f.read_to_string(&mut s)?;
    Ok(s)
}
1
2
3
4
5
6
7
8
9
10

?操作符跟在Result<T, E>类型之后,当Result值是Ok时,它的值会作为表达式的值返回;当值是Err时,会将这个错误作为整个函数的返回值抛出。

match表达式和?操作符还有一点不同:?操作符抛出的错误会经过一个由标准库Fromtrait提供的,名称为from的函数处理,它将原始的错误类型转换成我们当前函数声明中定义的错误类型。只要错误类型实现了from方法,?操作符就会调用它来进行错误类型转换。

?操作符使函数体更加简洁,上面这个例子还可以更加简洁。

use std::fs::File;
use std::io;
use std::io::Read;

fn read_username_from_file() -> Result<String, io::Error> {
    let mut s = String::new();
    File::open("hello.txt")?.read_to_string(&mut s)?;
    Ok(s)
}
1
2
3
4
5
6
7
8
9