-
Notifications
You must be signed in to change notification settings - Fork 3.7k
start of error handling #134
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 28 commits
a28453e
4723782
e7b5943
e35e0e1
80c9725
730fb1f
3634c81
ad0fcf3
0b7bb54
71d4ab9
bd18fff
6f3cf5c
9057190
3f9dd9d
e33f886
b86cf13
f9b690e
6f581c3
e8cba59
0f5a5e6
b2929bf
79bac99
b88aad0
7095fd1
cd7932f
9154816
2d40c73
48c6890
c1c40d5
e10d974
da9422b
6d2a2c4
e98eab8
48bed57
1eaa218
a3dfedb
34b346c
8f17a2a
68bf232
261056e
9cb2218
b7a9b8a
c00340c
6e7eb0e
211aec8
0fc14df
2162339
3a8c5ef
fdc98f4
0cd5b91
ee82377
234e758
3f72a06
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
# Error Handling | ||
|
||
Rust's laser-focus on safety spills over into a related area: error handling. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should start moving away from "safety" and toward "reliability" or "robustness" for this purpose. Safety is a somewhat specialized term -- and is easily thought of as something that higher-level languages just have. What we really want to get at here is that Rust cares about building reliable software across the board. |
||
Errors are a fact of life in software, so Rust has a number of features that you | ||
can use to handle situations in which something bad happens. In many cases, | ||
Rust requires you to acknowledge the possibility of an error occurring and take | ||
some action in that situation. This makes your program safer and more robust by | ||
eliminating the possibility of unexpected errors being discovered late in the | ||
development process. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think, more importantly, is the danger of only discovering possible errors after you've deployed. |
||
|
||
Rust groups errors into two major kinds: errors that are recoverable, and | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I still think we want to give some examples or additional intuition about this up front. E.g.:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree. I think at least a few examples will help the reader a lot in understanding those two terms. More examples:
Also: maybe set "recoverable" and "unrecoverable" in italic font throughout the whole section? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
We usually set new terms in italics the first time we use them, when we define them. Then later uses we just use the words regularly. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are we going with "unrecoverable" or "non-recoverable"? |
||
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? | ||
|
||
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 you can recover from. Finally, we'll discuss considerations | ||
to take into account when deciding whether to try to recover from an error or | ||
to stop execution. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
# Unrecoverable errors with 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 print a failure message and then quit. The most common reason for this is | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hm, so I know there was some contention as to whether to discuss unwinding here, but the current text feels... misleading... especially given that we unwind by default. I'd like to see either a sidebar about unwinding, or (probably better) a forward reference to material on unwinding that can come later. (That could just be a section in this chapter? I don't think we need to delve into a lot of detail; we can point people at the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Briefly mentioning unwinding sounds good to me. However, I wouldn't point to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I'm not sure we have a great place to discuss unwinding in a later chapter... maybe in the chapter about unsafe rust or FFI? I feel like I see people talking about I could see taking a condensed form of this section in this earlier version of the error handling chapter and making a sidebar on Which would be better, a sidebar here or a section in the later unsafe or FFI chapters? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think, to start, a short sidebar is a good move. We can see later on whether there's a logical place to give more details (e.g., talking about unwind safety) -- quite possibly in the FFI chapter. |
||
when a bug of some kind has been detected, and it's not clear how to handle the | ||
error. | ||
|
||
Let's try it out with a simple program: | ||
|
||
```rust,should_panic | ||
fn main() { | ||
panic!("crash and burn"); | ||
} | ||
``` | ||
|
||
If you run it, you'll see something like this: | ||
|
||
```bash | ||
$ cargo run | ||
Compiling panic v0.1.0 (file:///projects/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) | ||
``` | ||
|
||
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. | ||
|
||
But that only shows us the exact line that called `panic!`. That's not always | ||
useful. Let's modify our example slightly to see what it's like when a `panic!` | ||
call comes from code we call instead of from our code directly: | ||
|
||
```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. Using `[]` is supposed to return | ||
an element. If you pass `[]` an invalid index, though, there's no element that | ||
Rust could return here that would be correct. In this case, we've typed in a | ||
literal index of `100`, so in theory, Rust could examine our code at compile | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd drop the aside about the literal index here. Feels like a distraction. |
||
time and raise an error at that point. But if our program was different, say, | ||
maybe reading the index from user input, it would not be possible to determine | ||
at compile time if the index was in bounds. | ||
|
||
Other languages like C will attempt to give you exactly what you asked for in | ||
this situation, even though it isn't what you want: you'll get whatever is at | ||
the location in memory that would correspond to that element in the vector, | ||
even though the memory doesn't belong to the vector. This is called a *buffer | ||
overread*, and can lead to security vulnerabilities if an attacker can | ||
manipulate the index in such a way as to read data they shouldn't be allowed to | ||
that is stored after the array. | ||
|
||
In order to protect your program from this sort of vulnerability, if you try to | ||
read an element at an index that doesn't exist, Rust will stop execution and | ||
refuse to continue with an invalid value. Let's try it and see: | ||
|
||
```bash | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure if there is a global policy on this, but I don't think the bash syntax highlighting does any good. It's rather confusing IMO. (this affects other lines too) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hm. I'm going to file a new issue on this... I'm mostly doing it so that we get a gray box around this block in the mdbook output, but we could probably tweak our CSS to get plain text blocks to have a gray box too. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @steveklabnik i think the bash syntax highlighting is highlighting as if these blocks were bash scripts, not bash shell output, which is what we're putting in these blocks. It changes color of words in a seemingly arbitrary way for our output :-/ |
||
$ cargo run | ||
Compiling panic v0.1.0 (file:///projects/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 that: | ||
|
||
```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 /projects/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. The key to reading the backtrace is to start from the | ||
top and read until we see files that we wrote: that's where the problem | ||
originated. If we didn't want our program to panic here, this line is where we | ||
would start investigating in order to figure out how we got to this location | ||
with values that cause the panic. | ||
|
||
Now that we've covered how to `panic!` to stop our code's execution and how to | ||
debug a `panic!`, let's look at how to instead return and use recoverable | ||
errors with `Result`. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,220 @@ | ||
# Recoverable errors with `Result<T, E>` | ||
|
||
Most errors aren't so dire. Sometimes, when a function fails, it's for a reason | ||
that we can easily interpret and respond to. 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. | ||
|
||
In these cases, Rust's standard library provides an `enum` to use as the return | ||
type of the function: | ||
|
||
```rust | ||
enum Result<T, E> { | ||
Ok(T), | ||
Err(E), | ||
} | ||
``` | ||
|
||
The `Ok` variant indicates a successful result, and `Err` indicates an | ||
unsuccessful result. These two variants each contain one thing: in `Ok`'s case, | ||
it's the successful return value. With `Err`, it's some value that represents | ||
the error. The `T` and `E` are generic type parameters; we'll go into generics | ||
in more detail in Chapter XX. What you need to know for right now is that the | ||
`Result` type is defined such that it can have the same behavior for any type | ||
`T` that is what we want to return in the success case, and any type `E` that | ||
is what we want to return in the error case. | ||
|
||
As an example, let's try opening a file: | ||
|
||
```rust | ||
use std::fs::File; | ||
|
||
fn main() { | ||
let f = File::open("hello.txt"); | ||
} | ||
``` | ||
|
||
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. | ||
|
||
<!-- I'll ghost everything except the match statement lines in the libreoffice file /Carol --> | ||
|
||
```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 the `Ok` variant. 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 a message indicating as such when we | ||
print the error value: | ||
|
||
```bash | ||
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 on it to do various things. "Panic on an error result" is one of those | ||
methods, and it's called `unwrap()`: | ||
|
||
<!-- I'll ghost everything except `unwrap()` in the libreoffice file /Carol --> | ||
|
||
```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()` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I know, |
||
returns `Ok`, return the value inside. If it's an `Err`, panic. | ||
|
||
There's also another method, similar to `unwrap()`, but that lets us choose the | ||
error message: `expect()`. Using `expect()` instead of `unwrap()` and providing | ||
good error messages can convey your intent and make tracking down the source of | ||
a panic easier. `expect()` looks like this: | ||
|
||
<!-- I'll ghost everything except `expect()` in the libreoffice file /Carol --> | ||
|
||
```rust,should_panic | ||
use std::fs::File; | ||
|
||
fn main() { | ||
let f = File::open("hello.txt").expect("failed to open hello.txt"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why a different error message here than above ( There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mmmm probably copypasta! :) I'll make them be the same. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, I think the reason is because we'd have to have There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. UGH but then you don't get the |
||
} | ||
``` | ||
|
||
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 | ||
observation gets at an underlying truth: you can easily turn a recoverable | ||
error into an unrecoverable one with `unwrap()` or `expect()`, but you can't | ||
turn an unrecoverable `panic!` into a recoverable one. This is why good Rust | ||
code chooses to make errors recoverable: you give your caller choices. | ||
|
||
The Rust community has a love/hate relationship with `unwrap()` and `expect()`. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd also add that |
||
They're useful in tests since they will cause the test to fail if there's an | ||
error anyplace you call them. In examples, you might not want to muddy the code | ||
with proper error handling. But if you use them in a library, mis-using your | ||
library can cause other people's programs to halt unexpectedly, and that's not | ||
very user-friendly. | ||
|
||
## Propagating errors with `?` | ||
|
||
When writing a function, if you don't want to handle the error where you are, | ||
you can return the error to the calling function. Within your function, that | ||
would look like: | ||
|
||
<!-- I'll ghost everything except `return Err(e)` in the libreoffice file /Carol --> | ||
|
||
```rust,ignore | ||
# use std::fs::File; | ||
# fn foo() -> std::io::Result<()> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, why isn't Note also that you might want to avoid using the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure, I'll fix it! |
||
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: | ||
|
||
<!-- I'll ghost everything except `?` in the libreoffice file /Carol --> | ||
|
||
```rust,ignore | ||
#![feature(question_mark)] | ||
|
||
use std::fs::File; | ||
|
||
fn main() { | ||
let f = File::open("hello.txt")?; | ||
} | ||
``` | ||
|
||
The `?` operator at the end of the `open` call does the same thing as our | ||
previous example: It will return the value inside an `Ok` to the binding `f`, | ||
but will return early out of the whole function and give any `Err` value we get | ||
to our caller. | ||
|
||
There's one problem though; let's try compiling the example: | ||
|
||
```rust,ignore | ||
Compiling result v0.1.0 (file:///projects/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 `()`, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is confusing -- it's very unclear whether your examples have shifted to talking about a helper function or in main. In particular, if you're using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree. It's confusing to first talk about this Actually, I would already start with this In that function, I would first show how to do it manually with Those three things are all in the non- There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I think we started the book using a lot of examples that started in a "not working" state and we walked through how to get them working, but this has been confusing a lot of people. We have been changing to showing the working cases first and then talk about cases that might not work-- I'll change this one too. |
||
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` so | ||
that we are allowed to use the question mark operator: | ||
|
||
```rust | ||
#![feature(question_mark)] | ||
|
||
use std::fs::File; | ||
use std::io; | ||
|
||
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 both | ||
in our function signature. In this case, `File::open` returns `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; no value is returned when everything | ||
works. Functions with no return type, as we just saw with `main()`, are the | ||
same as returning the unit type, `()`. So we can use the unit type as the | ||
return type here, too. | ||
|
||
This leads us to the last line of the function, the slightly silly-looking | ||
`Ok(())`. This is an `Ok()` with a `()` value inside. | ||
|
||
Now we have the function `process_file` that uses the question mark operator and compiles successfully. We can call this function and handle the return value in any other function that returns a `Result` by also using the question mark operator there, and that would look like `process_file()?`. However, the `main` function still returns `()`, so we would still have to use a `match` statement, an `unwrap()`, or an `expect()` if we wanted to call `process_file` from `main`. Using `expect()` would look like this: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In general this section is pretty good. But one thing you don't illustrate is the error component of the |
||
|
||
```rust,ignore | ||
fn main() { | ||
process_file().expect("There was a problem processing the file"); | ||
} | ||
``` | ||
|
||
## Chaining Question Mark Operators | ||
|
||
TODO | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm still working on adding an example here along the lines of the one mitsuhiko quotes in discussions around |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this title is in ... title case. The other two sub-titles aren't. Is that just because this is a Shakespeare reference? if so, I'm cool with that, just making sure the others don't need to also be title case
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The others do need to be in title case. Good catch!