mirror of
https://github.com/rust-lang-cn/book-cn.git
synced 2025-01-26 09:18:42 +08:00
error handling take two
This commit is contained in:
parent
4723782107
commit
e7b5943c35
@ -5,26 +5,13 @@ Errors are a fact of life in software. Rust has a number of tools that you can
|
||||
use to handle things when something bad happens.
|
||||
|
||||
Rust splits errors into two major kinds: errors that are recoverable, and
|
||||
errors that are not recoverable. It has two different strategies to handle
|
||||
these two different forms of errors.
|
||||
|
||||
What does it mean to "recover" from an error? In the simplest sense, it
|
||||
relates to the answer of this question:
|
||||
errors that are not recoverable. What does it mean to "recover" from an
|
||||
error? In the simplest sense, it relates to the answer of this question:
|
||||
|
||||
> If I call a function, and something bad happens, can I do something
|
||||
> meaningful? Or should execution stop?
|
||||
|
||||
Some kinds of problems have solutions, but with other kinds of errors,
|
||||
all you can do is throw up your hands and give up.
|
||||
|
||||
We'll start off our examination of error handling by talking about the
|
||||
unrecoverable case first. Why? You can think of unrecoverable errors as a
|
||||
subset of recoverable errors. If you choose to treat an error as unrecoverable,
|
||||
then the function that calls your code has no choice in the matter. However, if
|
||||
your function returns a recoverable error, the calling function has a choice:
|
||||
handle the error properly, or convert it into an unrecoverable error. This is
|
||||
one of the reasons that most Rust code chooses to treat errors as recoverable:
|
||||
it's more flexible. We're going to explore an example that starts off as
|
||||
unrecoverable, and then, in the next section, we will convert it to a
|
||||
convertable error. Finally, we'll talk about how to create our own custom error
|
||||
types.
|
||||
The technique that you use depends on the answer to this question. First,
|
||||
we'll talk about `panic!`, Rust's way of signaling an unrecoverable error.
|
||||
Then, we'll talk about `Result<T, E>`, the return type for functions that
|
||||
may return an error, but one you can recover from.
|
||||
|
@ -1,36 +1,143 @@
|
||||
# Unrecoverable errors with panic!
|
||||
|
||||
You've already seen the way to signal an unrecoverable error: the `panic!`
|
||||
macro. Here's an example of using `panic!`:
|
||||
Sometimes, bad things happen, and there's nothing that you can do about it. For
|
||||
these cases, Rust has a macro, `panic!`. When this macro executes, your program
|
||||
will terminate execution, printing a failure message and then quitting. Try
|
||||
this program:
|
||||
|
||||
```rust
|
||||
fn check_guess(number: u32) -> bool {
|
||||
if number > 100 {
|
||||
panic!("Guess was too big: {}", number);
|
||||
}
|
||||
|
||||
number == 34
|
||||
```rust,should_panic
|
||||
fn main() {
|
||||
panic!("crash and burn");
|
||||
}
|
||||
```
|
||||
|
||||
This function accepts a guess between zero and a hundred, and checks if it's
|
||||
equivalent to the correct number, which is `34` in this case. It's kind of a
|
||||
silly function, but it's similar to a real example you've already seen:
|
||||
indexing vectors:
|
||||
If you run it, you'll see something like this:
|
||||
|
||||
```rust,should_panic
|
||||
let v = vec![1, 2, 3];
|
||||
|
||||
v[1000]; // this will panic
|
||||
```bash
|
||||
$ cargo run
|
||||
Compiling panic v0.1.0 (file:///home/steve/tmp/panic)
|
||||
Finished debug [unoptimized + debuginfo] target(s) in 0.25 secs
|
||||
Running `target/debug/panic`
|
||||
thread 'main' panicked at 'crash and burn', src/main.rs:2
|
||||
note: Run with `RUST_BACKTRACE=1` for a backtrace.
|
||||
error: Process didn't exit successfully: `target/debug/panic` (exit code: 101)
|
||||
```
|
||||
|
||||
The implementation of indexing with `[]` looks similar to our `check_guess`
|
||||
function above: check if the number is larger than the length of the vector,
|
||||
and if it is, panic.
|
||||
There are three lines of error message here. The first line shows our panic
|
||||
message and the place in our source code where the panic occurred:
|
||||
`src/main.rs`, line two.
|
||||
|
||||
Why do we need to panic? There's no number type for "between zero and a
|
||||
hundred" in Rust, so we are accepting a `u32`, and then checking in the
|
||||
function's body to make sure the guess is in-bounds. If the number is too big,
|
||||
there's been an error: someone made a mistake. We then invoke `panic!` to say
|
||||
"something went wrong, we cannot continue to run this program."
|
||||
But that only shows us the exact line that called `panic!`. That's not always
|
||||
useful. Let's modify our example slightly:
|
||||
|
||||
```rust,should_panic
|
||||
fn main() {
|
||||
let v = vec![1, 2, 3];
|
||||
|
||||
v[100];
|
||||
}
|
||||
```
|
||||
|
||||
We attempt to access the hundredth element of our vector, but it only has three
|
||||
elements. In this situation, Rust will panic. Let's try it:
|
||||
|
||||
```bash
|
||||
$ cargo run
|
||||
Compiling panic v0.1.0 (file:///home/steve/tmp/panic)
|
||||
Finished debug [unoptimized + debuginfo] target(s) in 0.27 secs
|
||||
Running `target/debug/panic`
|
||||
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is
|
||||
100', ../src/libcollections/vec.rs:1265
|
||||
note: Run with `RUST_BACKTRACE=1` for a backtrace.
|
||||
error: Process didn't exit successfully: `target/debug/panic` (exit code: 101)
|
||||
```
|
||||
|
||||
This points at a file we didn't write, `../src/libcollections/vec.rs`, line 1265.
|
||||
That's the implementation of `Vec<T>` in the standard library. While it's easy
|
||||
to see in this short program where the error was, it would be nicer if we could
|
||||
have Rust tell us what line in our program caused the error.
|
||||
|
||||
That's what the next line, the `note` is about. If we set the `RUST_BACKTRACE`
|
||||
environment variable, we'll get a backtrace of exactly how the error happend.
|
||||
Let's try it:
|
||||
|
||||
```bash
|
||||
$ RUST_BACKTRACE=1 cargo run
|
||||
Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
|
||||
Running `target/debug/panic`
|
||||
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is
|
||||
100', ../src/libcollections/vec.rs:1265
|
||||
stack backtrace:
|
||||
1: 0x560956150ae9 -
|
||||
std::sys::backtrace::tracing::imp::write::h482d45d91246faa2
|
||||
2: 0x56095615345c -
|
||||
std::panicking::default_hook::_{{closure}}::h89158f66286b674e
|
||||
3: 0x56095615291e - std::panicking::default_hook::h9e30d428ee3b0c43
|
||||
4: 0x560956152f88 -
|
||||
std::panicking::rust_panic_with_hook::h2224f33fb7bf2f4c
|
||||
5: 0x560956152e22 - std::panicking::begin_panic::hcb11a4dc6d779ae5
|
||||
6: 0x560956152d50 - std::panicking::begin_panic_fmt::h310416c62f3935b3
|
||||
7: 0x560956152cd1 - rust_begin_unwind
|
||||
8: 0x560956188a2f - core::panicking::panic_fmt::hc5789f4e80194729
|
||||
9: 0x5609561889d3 -
|
||||
core::panicking::panic_bounds_check::hb2d969c3cc11ed08
|
||||
10: 0x56095614c075 - _<collections..vec..Vec<T> as
|
||||
core..ops..Index<usize>>::index::hb9f10d3dadbe8101
|
||||
at ../src/libcollections/vec.rs:1265
|
||||
11: 0x56095614c134 - panic::main::h2d7d3751fb8705e2
|
||||
at /home/steve/tmp/panic/src/main.rs:4
|
||||
12: 0x56095615af46 - __rust_maybe_catch_panic
|
||||
13: 0x560956152082 - std::rt::lang_start::h352a66f5026f54bd
|
||||
14: 0x56095614c1b3 - main
|
||||
15: 0x7f75b88ed72f - __libc_start_main
|
||||
16: 0x56095614b3c8 - _start
|
||||
17: 0x0 - <unknown>
|
||||
error: Process didn't exit successfully: `target/debug/panic` (exit code: 101)
|
||||
```
|
||||
|
||||
That's a lot of output! Line `11` there has the line in our project:
|
||||
`src/main.rs` line four. We've been looking at the error message, but Cargo
|
||||
also told us something important about backtraces early on: `[unoptimized +
|
||||
debuginfo]`. 'debuginfo' is what enables the file names to be shown here.
|
||||
If we instead compile with `--release`:
|
||||
|
||||
```bash
|
||||
$ RUST_BACKTRACE=1 cargo run --release
|
||||
Compiling panic v0.1.0 (file:///home/steve/tmp/panic)
|
||||
Finished release [optimized] target(s) in 0.28 secs
|
||||
Running `target/release/panic`
|
||||
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is
|
||||
100', ../src/libcollections/vec.rs:1265
|
||||
stack backtrace:
|
||||
1: 0x565238fd0e79 -
|
||||
std::sys::backtrace::tracing::imp::write::h482d45d91246faa2
|
||||
2: 0x565238fd37ec -
|
||||
std::panicking::default_hook::_{{closure}}::h89158f66286b674e
|
||||
3: 0x565238fd2cae - std::panicking::default_hook::h9e30d428ee3b0c43
|
||||
4: 0x565238fd3318 -
|
||||
std::panicking::rust_panic_with_hook::h2224f33fb7bf2f4c
|
||||
5: 0x565238fd31b2 - std::panicking::begin_panic::hcb11a4dc6d779ae5
|
||||
6: 0x565238fd30e0 - std::panicking::begin_panic_fmt::h310416c62f3935b3
|
||||
7: 0x565238fd3061 - rust_begin_unwind
|
||||
8: 0x565239008dbf - core::panicking::panic_fmt::hc5789f4e80194729
|
||||
9: 0x565239008d63 -
|
||||
core::panicking::panic_bounds_check::hb2d969c3cc11ed08
|
||||
10: 0x565238fcc526 - panic::main::h2d7d3751fb8705e2
|
||||
11: 0x565238fdb2d6 - __rust_maybe_catch_panic
|
||||
12: 0x565238fd2412 - std::rt::lang_start::h352a66f5026f54bd
|
||||
13: 0x7f36aad6372f - __libc_start_main
|
||||
14: 0x565238fcc408 - _start
|
||||
15: 0x0 - <unknown>
|
||||
error: Process didn't exit successfully: `target/release/panic` (exit code:
|
||||
101)
|
||||
```
|
||||
|
||||
Now it just says 'optimized', and we don't have the file names any more. These
|
||||
settings are only the default; you can include debuginfo in a release build,
|
||||
or exclude it from a debug build, by configuring Cargo. See its documentation
|
||||
for more details: http://doc.crates.io/manifest.html#the-profile-sections
|
||||
|
||||
So why does Rust panic here? In this case, using `[]` is supposed to return
|
||||
a number. But if you pass it an invalid index, there's no number Rust could
|
||||
return here, it would be wrong. So the only thing that we can do is terminate
|
||||
the program.
|
||||
|
@ -1,116 +1,177 @@
|
||||
# Recoverable errors with `Result<T, E>`
|
||||
|
||||
The vast majority of errors in Rust are able to be recovered from. For this
|
||||
case, Rust has a special type, `Result<T, E>`, to signal that a function might
|
||||
succeed or fail.
|
||||
Most errors aren't so dire. When a function can fail for some reason, it'd be
|
||||
nice for it to not kill our entire program. As an example, maybe we are making
|
||||
a request to a website, but it's down for maintenance. In this situation, we'd
|
||||
like to wait and then try again. Terminating our process isn't the right thing
|
||||
to do here.
|
||||
|
||||
Let's take a look at our example function from the last section:
|
||||
In these cases, Rust's standard library provides an `enum` to use as the return
|
||||
type of the function:
|
||||
|
||||
```rust
|
||||
fn check_guess(number: u32) -> bool {
|
||||
if number > 100 {
|
||||
panic!("Guess was too big: {}", number);
|
||||
}
|
||||
|
||||
number == 34
|
||||
}
|
||||
```
|
||||
|
||||
We don't want the entire program to end if our `number` was incorrect. That's a
|
||||
bit harsh! Instead, we want to signal that an error occurred. Here's a version
|
||||
of `check_guess` that uses `Result<T, E>`:
|
||||
|
||||
```rust
|
||||
fn check_guess(number: u32) -> Result<bool, &'static str> {
|
||||
if number > 100 {
|
||||
return Err("Number was out of range");
|
||||
}
|
||||
|
||||
Ok(number == 34)
|
||||
}
|
||||
```
|
||||
|
||||
There are three big changes here: to the return type, to the error case, and to
|
||||
the non-error case. Let's look at each in turn.
|
||||
|
||||
```rust
|
||||
fn check_guess(number: u32) -> Result<bool, &'static str> {
|
||||
# if number > 100 {
|
||||
# return Err("Number was out of range");
|
||||
# }
|
||||
#
|
||||
# Ok(number == 34)
|
||||
# }
|
||||
```
|
||||
|
||||
Originally, we returned a `bool`, but now we return a
|
||||
`Result<bool, &'static str>`. This is a type [provided by the standard library]
|
||||
specifically for indicating that a function might have an error. More
|
||||
specifically, it's an [`enum`] that looks like this:
|
||||
|
||||
```rust
|
||||
pub enum Result<T, E> {
|
||||
enum Result<T, E> {
|
||||
Ok(T),
|
||||
Err(E),
|
||||
}
|
||||
```
|
||||
|
||||
[provided by the standard library]: https://doc.rust-lang.org/stable/std/result/enum.Result.html
|
||||
[`enum]`: ch06-01-enums.html
|
||||
We have `Ok` for successful results, and `Err` for ones that have an error.
|
||||
These two variants each contain one thing: in `Ok`'s case, it's the successful
|
||||
return value. With `Err`, it's some type that represents the error.
|
||||
|
||||
`Result<T, E>` is generic over two types: `T`, which is the successful case, and
|
||||
`E`, which is the error case. It has two variants, `Ok` and `Err`, which also
|
||||
correspond to these cases, respectively. So the type `Result<bool, &'static
|
||||
str>` means that in the successful, `Ok` case, we will be returning a `bool`.
|
||||
But in the failure, `Err` case, we will be returning a string literal.
|
||||
As an example, let's try opening a file:
|
||||
|
||||
```rust
|
||||
# fn check_guess(number: u32) -> Result<bool, &'static str> {
|
||||
# if number > 100 {
|
||||
return Err("Number was out of range");
|
||||
# }
|
||||
#
|
||||
# Ok(number == 34)
|
||||
# }
|
||||
```
|
||||
|
||||
The second change we need to make is to our error case. Instead of causing a
|
||||
`panic!`, we now `return` an `Err`, with a string literal inside. Remember,
|
||||
`Result<T, E>` is an enum: `Err` is one of its variants.
|
||||
|
||||
```rust
|
||||
# fn check_guess(number: u32) -> Result<bool, &'static str> {
|
||||
# if number > 100 {
|
||||
# return Err("Number was out of range");
|
||||
# }
|
||||
#
|
||||
Ok(number == 34)
|
||||
# }
|
||||
```
|
||||
|
||||
We also need to handle the successful case as well, and we make a change
|
||||
similarly to the error case: we wrap our return value in `Ok`, which gives it
|
||||
the correct type.
|
||||
|
||||
## Handling an error
|
||||
|
||||
Let's examine how to use this function:
|
||||
|
||||
```rust
|
||||
fn check_guess(number: u32) -> Result<bool, &'static str> {
|
||||
if number > 100 {
|
||||
return Err("Number was out of range");
|
||||
}
|
||||
|
||||
Ok(number == 34)
|
||||
}
|
||||
use std::fs::File;
|
||||
|
||||
fn main() {
|
||||
let answer = check_guess(5);
|
||||
let f = File::open("hello.txt");
|
||||
}
|
||||
```
|
||||
|
||||
match answer {
|
||||
Ok(b) => println!("answer: {}", b),
|
||||
Err(e) => println!("There was an error: {}, e"),
|
||||
The `open` function returns a `Result`: there are many ways in which opening
|
||||
a file can fail. For example, unless we created `hello.txt`, this file does
|
||||
not yet exist. Before we can do anything with our `File`, we need to extract
|
||||
it out of the result. Let's start with a basic tool: `match`. We've used it
|
||||
to deal with enums previously.
|
||||
|
||||
```rust,should_panic
|
||||
use std::fs::File;
|
||||
|
||||
fn main() {
|
||||
let f = File::open("hello.txt");
|
||||
|
||||
let f = match f {
|
||||
Ok(file) => file,
|
||||
Err(error) => panic!("There was a problem opening the file: {:?}",
|
||||
error),
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
If we see an `Ok`, we can return the inner `file` out of it. If we see `Err`,
|
||||
we have to decide what to do with it. The simplest thing is to turn our error
|
||||
into a `panic!` instead, by calling the macro. And since we haven't created
|
||||
that file yet, we'll see it in the error message:
|
||||
|
||||
```text
|
||||
thread 'main' panicked at 'There was a problem opening the file: Error { repr:
|
||||
Os { code: 2, message: "No such file or directory" } }', src/main.rs:8
|
||||
```
|
||||
|
||||
This works okay. However, `match` can be a bit verbose, and it doesn't always
|
||||
communicate intent well. The `Result<T, E>` type has many helper methods
|
||||
defined it to do various things. "Panic on an error result" is one of those
|
||||
methods, and it's called `unwrap`:
|
||||
|
||||
```rust,should_panic
|
||||
use std::fs::File;
|
||||
|
||||
fn main() {
|
||||
let f = File::open("hello.txt").unwrap();
|
||||
}
|
||||
```
|
||||
|
||||
This has the same behavior as our previous example: If the call to `open`
|
||||
returns `Ok`, return the value inside. If it's an `Err`, panic.
|
||||
|
||||
This isn't the only way to deal with errors, however. This entire section is
|
||||
supposed to be about recovering from errors, but we've gone back to panic.
|
||||
This is true, and gets at an underlying truth: you can easily turn a
|
||||
recoverable error into an unrecoverable one with `unwrap`, but you can't turn
|
||||
an unrecoverable error into a recoverable one. This is why good Rust code
|
||||
chooses to make errors recoverable: you give your caller options.
|
||||
|
||||
The Rust community has a love/hate relationship with `unwrap`. It's useful
|
||||
in tests, and in examples where you don't want to muddy the example with proper
|
||||
error handling. But if used in library code, mis-using that library can cause
|
||||
your program to blow up, and that's not good.
|
||||
|
||||
## Propagating errors with `?`
|
||||
|
||||
Sometimes, when writing a function, you don't want to handle the error where
|
||||
you are, but instead would prefer to return the error to the calling function.
|
||||
Something like this:
|
||||
|
||||
```rust
|
||||
use std::fs::File;
|
||||
|
||||
# fn foo() -> std::io::Result<()> {
|
||||
let f = File::open("hello.txt");
|
||||
|
||||
let f = match f {
|
||||
Ok(file) => file,
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
|
||||
# Ok(())
|
||||
# }
|
||||
```
|
||||
|
||||
This is a very common way of handling errors: propagate them upward until
|
||||
you're ready to deal with them. This pattern is so common in Rust that there is
|
||||
dedicated syntax for it: the question mark operator. We could have also written
|
||||
the example like this:
|
||||
|
||||
```rust
|
||||
#![feature(question_mark)]
|
||||
|
||||
use std::fs::File;
|
||||
|
||||
# fn foo() -> std::io::Result<()> {
|
||||
let f = File::open("hello.txt")?;
|
||||
# Ok(())
|
||||
# }
|
||||
```
|
||||
|
||||
The `?` operator at the end of the `open` call does the same thing as our
|
||||
previous example: It will return the value of an `Ok`, but return the value of
|
||||
an `Err` to our caller.
|
||||
|
||||
There's one problem though: let's try compiling the example:
|
||||
|
||||
```rust,ignore
|
||||
Compiling result v0.1.0 (file:///home/steve/tmp/result)
|
||||
error[E0308]: mismatched types
|
||||
--> src/main.rs:6:13
|
||||
|
|
||||
6 | let f = File::open("hello.txt")?;
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ expected (), found enum
|
||||
`std::result::Result`
|
||||
|
|
||||
= note: expected type `()`
|
||||
= note: found type `std::result::Result<_, _>`
|
||||
|
||||
error: aborting due to previous error
|
||||
```
|
||||
|
||||
What gives? The issue is that the `main()` function has a return type of `()`,
|
||||
but the question mark operator is trying to return a result. This doesn't work.
|
||||
Instead of `main()`, let's create a function that returns a result:
|
||||
|
||||
```rust
|
||||
#![feature(question_mark)]
|
||||
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
|
||||
fn main() {
|
||||
}
|
||||
|
||||
pub fn process_file() -> Result<(), io::Error> {
|
||||
let f = File::open("hello.txt")?;
|
||||
|
||||
// do some stuff with f
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Since the result type has two type parameters, we need to include them. In this
|
||||
case, `File::open` returns an `std::io::Error`, so we will use it as our error
|
||||
type. But what about success? This function is executed purely for its side
|
||||
effects; nothing is returned upon success. Well, functions with no return type,
|
||||
as we just saw with `main()`, are the same as returning unit. So we can use
|
||||
it as the return type here, too. This leads to the last line of the function,
|
||||
the slightly silly-looking `Ok(())`. This is an `Ok()` with a `()` inside.
|
||||
|
@ -1 +1,34 @@
|
||||
# Creating your own Error types
|
||||
|
||||
This pattern of "return an error" is so common, many libraries create their
|
||||
own error type, and use it for all of their functions. We can re-write the
|
||||
previous example to use `std::io::Result` rathern than a regular `Result`:
|
||||
|
||||
```rust
|
||||
#![feature(question_mark)]
|
||||
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
|
||||
fn main() {
|
||||
}
|
||||
|
||||
pub fn process_file() -> io::Result<()> {
|
||||
let f = File::open("hello.txt")?;
|
||||
|
||||
// do some stuff with f
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
`io::Result` looks like this:
|
||||
|
||||
```rust
|
||||
# use std::io;
|
||||
type Result<T> = Result<T, std::io::Error>;
|
||||
```
|
||||
|
||||
It's a type alias for a regular-old `Result<T, E>`, with the `E` set up to be a
|
||||
`std::io::Error`. This means we don't need to worry about the error type in our
|
||||
function signatures, which is nice.
|
||||
|
Loading…
Reference in New Issue
Block a user