error handling take two

This commit is contained in:
Steve Klabnik 2016-08-23 14:51:45 -04:00 committed by Carol (Nichols || Goulding)
parent 4723782107
commit e7b5943c35
4 changed files with 329 additions and 141 deletions

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.