rust-book-cn/nostarch/chapter09.md
Carol (Nichols || Goulding) 0089111432 Ship chapter 9 to nostarch
2016-10-31 11:57:58 -04:00

28 KiB

[TOC]

Error Handling

Rust's focus on reliability extends to the area of error handling. 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 more robust by eliminating the possibility of unexpected errors only being discovered after you've deployed your code to production.

Rust groups errors into two major kinds: errors that are recoverable, and errors that are unrecoverable. Recoverable errors are problems like a file not being found, where it's usually reasonable to report that problem to the user and retry the operation. Unrecoverable errors are problems like trying to access a location beyond the end of an array, and these are always symptoms of bugs.

Most languages do not distinguish between the two kinds of errors, so they handle both kinds in the same way using mechanisms like exceptions. Rust doesn't have exceptions. Instead, it has the value Result<T, E> to return in the case of recoverable errors and the panic! macro that stops execution when it encounters unrecoverable errors. This chapter will cover the more straightforward case of calling panic! first. Then, we'll talk about returning Result<T, E> values and calling functions that return Result<T, E>. Finally, we'll discuss considerations to take into account when deciding whether to try to recover from an error or to stop execution.

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, unwind and clean up the stack, and then quit. The most common reason for this is when a bug of some kind has been detected, and it's not clear how to handle the error.

Unwinding

By default, when a panic! happens in Rust, the program starts unwinding, which means Rust walks back up the stack and cleans up the data from each function it encounters. Doing that walking and cleanup is a lot of work. The alternative is to immediately abort, which ends the program without cleaning up. Memory that the program was using will need to be cleaned up by the operating system. If you're in a situation where you need to make the resulting binary as small as possible, you can switch from unwinding on panic to aborting on panic by adding panic = 'abort' to the appropriate [profile] sections in your Cargo.toml.

Let's try out calling panic!() with a simple program:

fn main() {
    panic!("crash and burn");
}

If you run it, you'll see something like this:

$ 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 look at another example to see what it's like when a panic! call comes from code we call instead of from our code directly:

fn main() {
    let v = vec![1, 2, 3];

    v[100];
}

We're attempting 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.

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:

$ 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. 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. Listing 9-1 shows the output:

$ 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)
Listing 9-1: The backtrace generated by a call to `panic!` displayed when the environment variable `RUST_BACKTRACE` is set

That's a lot of output! Line 11 of the backtrace points to the line in our project causing the problem: 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 caused 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.

Recoverable Errors with Result

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:

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.

Listing 9-2 shows an example of something that might fail: opening a file.

use std::fs::File;

fn main() {
    let f = File::open("hello.txt");
}
Listing 9-2: Opening a file

The type of f in this example is a Result, because 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. Listing 9-3 shows one way to handle the Result with a basic tool: the match expression that we learned about in Chapter 6.

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),
    };
}
Listing 9-3: Using a `match` expression to handle the `Result` variants we might have

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:

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

Matching on Different Errors

There are many reasons why opening a file might fail, and we may not want to take the same actions to try to recover for all of them. For example, if the file we're trying to open does not exist, we could choose to create it. If the file exists but we don't have permission to read it, or any other error, we still want to panic! in the same way as above and not create the file.

The Err type File::open returns is io::Error, which is a struct provided by the standard library. This struct has a method kind that we can call to get an io::ErrorKind value that we can use to handle different causes of an Err returned from File::open differently as in Listing 9-4:

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

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

    let f = match f {
        Ok(file) => file,
        Err(ref error) if error.kind() == ErrorKind::NotFound => {
            match File::create("hello.txt") {
                Ok(fc) => fc,
                Err(e) => panic!("Tried to create file but there was a problem: {:?}", e),
            }
        },
        Err(error) => panic!("There was a problem opening the file: {:?}",
error),
    };
}
Listing 9-4: Handling different kinds of errors in different ways

This example uses a match guard with the second arm's pattern to add a condition that further refines the pattern. The ref in the pattern is needed so that the error is not moved into the guard condition. The condition we want to check is that the value error.kind() returns is the NotFound variant of the ErrorKind enum. Note that File::create could also fail, so we need to add an inner match statement as well! The last arm of the outer match stays the same to panic on any error besides the file not being found.

Shortcuts for Panic on Error: unwrap and expect

Using match works okay but 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():

use std::fs::File;

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

This has similar behavior as the example using match in Listing 9-3: If the call to open() returns Ok, return the value inside. If it's an Err, panic.

There's also another method that is similar to unwrap(), but 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:

use std::fs::File;

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

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(). They're very handy when prototyping, before you're ready to decide how to handle errors, and in that case they leave clear markers to look for when you are ready to make your program more robust. They're useful in tests since they will cause the test to fail if there's an error any place 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.

Another time it's appropriate to call unwrap is when we have some other logic that ensures the Result will have an Ok value, but the logic isn't something the compiler understands. If you can ensure by manually inspecting the code that you'll never have an Err variant, it is perfectly acceptable to call unwrap. Here's an example:

use std::net::IpAddr;

let home = "127.0.0.1".parse::<IpAddr>().unwrap();

We're creating an IpAddr instance by parsing a hardcoded string. We can see that "127.0.0.1" is a valid IP address, so it's acceptable to use unwrap here. If we got the IP address string from a user of our program instead of hardcoding this value, we'd definitely want to handle the Result in a more robust way instead.

Propagating errors with try! or ?

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. For example, Listing 9-5 shows a function that reads a username from a file. If the file doesn't exist or can't be read, this function will return those errors to the code that called this function:

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),
    }
}
Listing 9-5: A function that returns errors to the calling code using `match`

Since the Result type has two type parameters, we need to include them both in our function signature. In this case, File::open and read_to_string return std::io::Error as the value inside the Err variant, so we will also use it as our error type. If this function succeeds, we want to return the username as a String inside the Ok variant, so that is our success type.

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 a macro for it, try!, and as of Rust 1.14 , dedicated syntax for it: the question mark operator. We could have written the code in Listing 9-5 using the try! macro, as in Listing 9-6, and it would have the same functionality as the match expressions:

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

    try!(f.read_to_string(&mut s));

    Ok(s)
}
Listing 9-6: A function that returns errors to the calling code using `try!`

Or as in Listing 9-7, which uses the question mark operator:

#![feature(question_mark)]

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)
}
Listing 9-7: A function that returns errors to the calling code using `?`

The ? operator at the end of the open call does the same thing as the example that uses match and the example that uses the try! macro: 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. The same thing applies to the ? at the end of the read_to_string call.

The advantage of using the question mark operator over the try! macro is the question mark operator permits chaining. We could further shorten this code by instead doing:

#![feature(question_mark)]

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)
}

Much nicer, right? The try! macro and the ? operator make propagating errors upwards much more ergonomic. There's one catch though: they can only be used in functions that return a Result, since they expand to the same match expression we saw above that had a potential early return of an Err value. Let's look at what happens if we try to use try! in the main function, which you'll recall has a return type of ():

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

When we compile this, we get the following error message:

error[E0308]: mismatched types
 -->
  |
3 |     let f = try!(File::open("hello.txt"));
  |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected (), found enum `std::result::Result`
  |
  = note: expected type `()`
  = note:    found type `std::result::Result<_, _>`

The mismatched types that this error is pointing out says the main() function has a return type of (), but the try! macro might return a Result. So in functions that don't return Result, when you call other functions that return Result, you'll need to use a match or one of the methods on Result to handle it instead of using try! or ?.

Now that we've discussed the details of calling panic! or returning Result, let's return to the topic of how to decide which is appropriate in which cases.

To panic! or Not To panic!

So how do you decide when you should call panic! and when you should return Result? A good default for a function that might fail is to return Result since that gives the caller of your function the most flexibility.

But that answer is simplistic. There are cases where you might want to call panic! in library code that have to do with Rust's quest for safety. Let's look at some more nuanced guidelines.

Guidelines for Error Handling

panic! when your code is in a situation where it's possible to be in a bad state and:

  • The bad state is not something that's expected to happen occasionally
  • Your code after this point needs to rely on not being in this bad state
  • There's not a good way to encode this information in the types you use

By bad state, we mean some assumption, guarantee, contract, or invariant has been broken. Some examples are invalid values, contradictory values, or nothing when you expect to have something. If someone calls your code and passes in values that don't make sense, the best thing might be to panic! and alert the person using your library to the bug in their code so that they can fix it during development. Similarly, panic! is often appropriate if you call someone else's code that is out of your control, and it returns an invalid state that you have no way of fixing.

Taking each point in turn:

Some bad states are expected to happen sometimes, and will happen no matter how well you write your code. Examples of this include a parser being given malformed data to parse, or an HTTP request returning a status that indicates you have hit a rate limit. In these cases, you should indicate that failure is an expected possibility by returning a Result and propagate these bad states upwards so that the caller can decide how they would like to handle the problem. panic! would not be the best way to handle these cases.

When your code performs operations on values, your code should verify the values are valid first, then proceed confidently with the operations. This is mostly for safety reasons: attempting to operate on invalid data can expose your code to vulnerabilities. This is the main reason that the standard library will panic! if you attempt an out-of-bounds array access: trying to access memory that doesn't belong to the current data structure is a common security problem. Functions often have contracts: their behavior is only guaranteed if the inputs meet particular requirements. Panicking when the contract is violated makes sense because a contract violation always indicates a caller-side bug, and it is not a kind of error you want callers to have to explicitly handle. In fact, there's no reasonable way for calling code to recover: the calling programmers need to fix the code. Contracts for a function, especially when a violation will cause a panic, should be explained in the API documentation for the function.

Having lots of error checks in all of your functions would be verbose and annoying, though. Luckily, our last guideline has a tip for this situation: use Rust's type system (and thus the type checking the compiler does) to do a lot of the checks for you. If your function takes a particular type as an argument, you can proceed with your code's logic knowing that the compiler has already ensured you have a valid value. For example, if you have a type rather than an Option, you know that you will have something rather than nothing and you don't have to have an explicit check to make sure. Another example is using an unsigned integer type like u32, which ensures the argument value is never negative.

Creating Custom Types for Validation

Going a step further with the idea of using Rust's type system to ensure we have a valid value, let's look at an example of creating a custom type for validation. Recall the guessing game in Chapter 2, where our code asked the user to guess a number between 1 and 100. We actually never validated that the user's guess was between those numbers before checking it against our secret number, only that it was positive. In this case, the consequences were not very dire: our output of "Too high" or "Too low" would still be correct. It would be a nice enhancement to guide the user towards valid guesses, though. We could add a check after we parse the guess:

loop {
    // snip

    let guess: u32 = match guess.trim().parse() {
        Ok(num) => num,
        Err(_) => continue,
    };

    if guess < 1 || guess > 100 {
        println!("The secret number will be between 1 and 100.");
        continue;
    }

    match guess.cmp(&secret_number) {
    // snip
}

The if expression checks to see if our value is out of range, tells the user about the problem, and calls continue to start the next iteration of the loop and ask for another guess. After the if expression, we can proceed with the comparisons between guess and the secret number knowing that guess is between 1 and 100.

If we had a situation where it was absolutely critical we had a value between 1 and 100, and we had many functions that had this requirement, it would be tedious (and potentially impact performance) to have a check like this in every function. Instead, we can make a new type and put the validations in one place, in the type's constructor. Then our functions can use the type with the confidence that we have values that meet our requirements. Listing 9-8 shows one way to define a Guess type that will only create an instance of Guess if the new function gets a value between 1 and 100:

struct Guess {
    value: u32,
}

impl Guess {
    pub fn new(value: u32) -> Guess {
        if value < 1 || value > 100 {
            panic!("Guess value must be between 1 and 100, got {}.", value);
        }

        Guess {
            value: value,
        }
    }

    pub fn value(&self) -> u32 {
        self.value
    }
}
Listing 9-8: A `Guess` type that will only hold values between 1 and 100

If code calling Guess::new passed in a value that was not between 1 and 100, that would be a violation of the contract that Guess::new is relying on. This function needs to signal to the calling code that it has a bug somewhere leading to the contract violation. The conditions in which Guess::new might panic should be discussed in its public-facing API documentation, which we will cover in Chapter XX.

Important to note is the value field of the Guess struct is private, so code using this struct may not set that value directly. Callers must use the Guess::new constructor function to create an instance of Guess, and they may read the value using the public value function, but they may not access the field directly. This means any created instance of Guess that does not cause a panic! when new is called is guaranteed to return numbers between 1 and 100 from its value function.

A function that takes as an argument or returns only numbers between 1 and 100 could then declare in its signature to take a Guess rather than a u32, and would not need to do any additional checks in its body.

Summary

Rust's error handling features are designed to help you write more robust code. The panic! macro signals that your program is in a state it can't handle, and lets you tell the process to stop instead of trying to proceed with invalid or incorrect values. The Result enum uses Rust's type system as a sign that operations you call might fail in a way that your code could recover from. You can use Result to tell code that calls yours that it needs to handle potential success or failure as well. Using panic! and Result in the appropriate situations will help your code be more reliable in the face of inevitable problems.

Now that we've seen useful ways that the standard library uses generics with the Option and Result enums, let's talk about how generics work and how you can make use of them in your code.