Contents

Ch.9 — Error Handling

Chapter 9

Error Handling


Errors are a fact of life in software, so Rust has a number of features for handling situations in which something goes wrong. In many cases, Rust requires you to acknowledge the possibility of an error and take some action before your code will compile. This requirement makes your program more robust by ensuring that you’ll discover errors and handle them appropriately before deploying your code to production!

The chapter covers calling panic! first, then returning Result<T, E> values, and finally how to decide whether to try to recover from an error or to stop execution.

  • 9.1 Unrecoverable Errors with panic!
  • 9.2 Recoverable Errors with Result
  • 9.3 To panic! or Not to panic!

The Rust Programming Language · Ch.9 Error Handling

1

Chapter 9 · Introduction

Two kinds of errors: recoverable and unrecoverable

Rust groups errors into two major categories:

Definition 9.1 (Recoverable error). An error such as a file not found error, where we most likely just want to report the problem to the user and retry the operation.

Definition 9.2 (Unrecoverable error). An error that is always a symptom of a bug, such as trying to access a location beyond the end of an array; we want to immediately stop the program.

Most languages don’t distinguish between these two kinds of errors and handle both in the same way, using mechanisms such as exceptions. Rust doesn’t have exceptions. Instead:

Table 9.1: How Rust handles each category

CategoryRust mechanism
RecoverableThe type Result<T, E>
UnrecoverableThe panic! macro, which stops execution

The Rust Programming Language · Ch.9 Error Handling

2

Chapter 9 · 9.1 Unrecoverable Errors with panic!

The panic! macro: unwinding or aborting

Sometimes bad things happen in your code, and there’s nothing you can do about it. For these cases Rust has the panic! macro. There are two ways to cause a panic in practice:

  • by taking an action that causes our code to panic (such as accessing an array past the end), or
  • by explicitly calling the panic! macro.

By default, these panics print a failure message, unwind, clean up the stack, and quit. Via an environment variable, you can also have Rust display the call stack when a panic occurs, to make it easier to track down the source of the panic.

Definition 9.3 (Unwinding). The default response to a panic: Rust walks back up the stack and cleans up the data from each function it encounters. Walking back and cleaning up is a lot of work.

Definition 9.4 (Aborting). The alternative you may choose: immediately ending the program without cleaning up. Memory the program was using must then be cleaned up by the operating system.

The Rust Programming Language · Ch.9 Error Handling

3

Chapter 9 · 9.1 Unrecoverable Errors with panic!

Aborting on panic, and calling panic! directly

If your project needs the resultant binary to be as small as possible, switch from unwinding to aborting upon a panic by adding panic = 'abort' to the appropriate [profile] sections in your Cargo.toml file. For example, to abort on panic in release mode, add this:

[profile.release]

panic = 'abort'

Let’s try calling panic! in a simple program:

src/main.rs

fn main() {

    panic!("crash and burn");

}

The Rust Programming Language · Ch.9 Error Handling

4

Chapter 9 · 9.1 Unrecoverable Errors with panic!

Reading a panic message

When you run the program, you’ll see something like this:

$ cargo run

   Compiling panic v0.1.0 (file:///projects/panic)

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.25s

     Running `target/debug/panic`

 

thread 'main' (6018279) panicked at src/main.rs:2:5:

crash and burn

note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

  • The call to panic! causes the error message in the last two lines. The first of them shows our panic message and the place in our source code where the panic occurred: src/main.rs:2:5 means the second line, fifth character of src/main.rs.
  • Here the line indicated is part of our code: going there, we see the panic! call. In other cases the panic! call might be in code that our code calls; the filename and line number reported will then be someone else’s code where panic! is called, not the line of our code that eventually led to it.

The Rust Programming Language · Ch.9 Error Handling

5

Chapter 9 · 9.1 Unrecoverable Errors with panic!

A panic from a library: indexing past the end

We can use the backtrace of the functions the panic! call came from to figure out the part of our code causing the problem. To see how, look at an example where a panic! call comes from a library because of a bug in our code, instead of from our code calling the macro directly. Listing 9.1 tries to access an index in a vector beyond the range of valid indexes.

Listing 9.1: Attempting to access an element beyond the end of a vector, which will cause a call to panic! — src/main.rs

fn main() {

    let v = vec![1, 2, 3];

 

    v[99];

}

Here we’re attempting to access the 100th element of our vector (at index 99, because indexing starts at zero), but the vector has only three elements. In this situation, Rust will panic. Using [] is supposed to return an element, but if you pass an invalid index, there’s no element that Rust could return here that would be correct.

The Rust Programming Language · Ch.9 Error Handling

6

Chapter 9 · 9.1 Unrecoverable Errors with panic!

Why Rust stops instead of reading past the end

In C, reading beyond the end of a data structure is undefined behavior: you might get whatever is at that element’s memory location, even though the memory doesn’t belong to the structure.

Definition 9.5 (Buffer overread). Reading memory past the end of a data structure. It can lead to security vulnerabilities if an attacker can manipulate the index so as to read data stored after the data structure that they shouldn’t be allowed to read.

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. Let’s try it and see:

$ cargo run

   Compiling panic v0.1.0 (file:///projects/panic)

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.27s

     Running `target/debug/panic`

 

thread 'main' (6017887) panicked at src/main.rs:4:6:

index out of bounds: the len is 3 but the index is 99

note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

The Rust Programming Language · Ch.9 Error Handling

7

Chapter 9 · 9.1 Unrecoverable Errors with panic!

Backtraces and how to read them

This error points at line 4 of our main.rs, where we attempt to access index 99 of the vector in v. The note: line tells us that we can set the RUST_BACKTRACE environment variable to get a backtrace of exactly what happened to cause the error.

Definition 9.6 (Backtrace). A list of all the functions that have been called to get to this point.

Backtraces in Rust work as they do in other languages:

  • The key to reading the backtrace is to start from the top and read until you see files you wrote. That’s the spot where the problem originated.
  • The lines above that spot are code that your code has called; the lines below are code that called your code.
  • These before-and-after lines might include core Rust code, standard library code, or crates that you’re using.

To get a backtrace, set the RUST_BACKTRACE environment variable to any value except 0. Listing 9.2 shows output similar to what you’ll see.

The Rust Programming Language · Ch.9 Error Handling

8

Chapter 9 · 9.1 Unrecoverable Errors with panic!

A backtrace with RUST_BACKTRACE=1

Listing 9.2: The backtrace generated by a call to panic! displayed when the environment variable RUST_BACKTRACE is set

$ RUST_BACKTRACE=1 cargo run

thread 'main' panicked at src/main.rs:4:6:

index out of bounds: the len is 3 but the index is 99

stack backtrace:

   0: rust_begin_unwind

             at /rustc/4d91de4e48198da2e33413efdcd9cd2cc0c46688/library/std/src/panicking.rs:692:5

   1: core::panicking::panic_fmt

             at /rustc/4d91de4e48198da2e33413efdcd9cd2cc0c46688/library/core/src/panicking.rs:75:14

   2: core::panicking::panic_bounds_check

             at /rustc/4d91de4e48198da2e33413efdcd9cd2cc0c46688/library/core/src/panicking.rs:273:5

   3: <usize as core::slice::index::SliceIndex<[T]>>::index

             at file:///home/.rustup/toolchains/1.85/lib/rustlib/

                    src/rust/library/core/src/slice/index.rs:274:10

(continued on the next slide; long paths are wrapped onto an indented second line)

The Rust Programming Language · Ch.9 Error Handling

9

Chapter 9 · 9.1 Unrecoverable Errors with panic!

A backtrace with RUST_BACKTRACE=1 (continued)

Listing 9.2 (continued)

   4: core::slice::index::<impl core::ops::index::Index<I> for [T]>::index

             at file:///home/.rustup/toolchains/1.85/lib/rustlib/

                    src/rust/library/core/src/slice/index.rs:16:9

   5: <alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index

             at file:///home/.rustup/toolchains/1.85/lib/rustlib/

                    src/rust/library/alloc/src/vec/mod.rs:3361:9

   6: panic::main

             at ./src/main.rs:4:6

   7: core::ops::function::FnOnce::call_once

             at file:///home/.rustup/toolchains/1.85/lib/rustlib/

                    src/rust/library/core/src/ops/function.rs:250:5

note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

That’s a lot of output! The exact output you see might differ depending on your operating system and Rust version. To get backtraces with this information, debug symbols must be enabled. They are enabled by default when using cargo build or cargo run without the --release flag, as we have here.

The Rust Programming Language · Ch.9 Error Handling

10

Chapter 9 · 9.1 Unrecoverable Errors with panic!

From the backtrace to a fix

  • In Listing 9.2, line 6 of the backtrace (6: panic::main) points to the line in our project that’s causing the problem: line 4 of src/main.rs.
  • If we don’t want our program to panic, we should start our investigation at the location pointed to by the first line mentioning a file we wrote.
  • In Listing 9.1, where we deliberately wrote code that would panic, the way to fix the panic is to not request an element beyond the range of the vector indexes.
  • When your code panics in the future, you’ll need to figure out what action the code is taking with what values to cause the panic, and what the code should do instead.

Remark. We’ll come back to panic! — and when we should and should not use it to handle error conditions — in Section 9.3, “To panic! or Not to panic!”. Next, we look at how to recover from an error using Result.

The Rust Programming Language · Ch.9 Error Handling

11

Chapter 9 · 9.2 Recoverable Errors with Result

The Result enum

Most errors aren’t serious enough to stop the program entirely; often a function fails for a reason you can easily interpret and respond to. If opening a file fails because it doesn’t exist, you might create the file instead of terminating.

Recall from Chapter 2 (“Handling Potential Failure with Result”) that the Result enum is defined as having two variants, Ok and Err:

enum Result<T, E> {

    Ok(T),

    Err(E),

}

Definition 9.7 (Result<T, E>). An enum whose generic type parameters (Chapter 10) are T, the type of the value returned in a success case within the Ok variant, and E, the type of the error returned in a failure case within the Err variant.

Thanks to these generic type parameters, Result and the functions defined on it work in many situations where the success and error values we want to return differ.

The Rust Programming Language · Ch.9 Error Handling

12

Chapter 9 · 9.2 Recoverable Errors with Result

Opening a file returns a Result

Let’s call a function that returns a Result value because it could fail. In Listing 9.3, we try to open a file.

Listing 9.3: Opening a file — src/main.rs

use std::fs::File;

 

fn main() {

    let greeting_file_result = File::open("hello.txt");

}

  • The return type of File::open is a Result<T, E>. T is filled in with the success type std::fs::File, a file handle; E is std::io::Error.
  • The call might succeed and return a file handle we can read from or write to. It also might fail: the file might not exist, or we might not have permission to access it. File::open must tell us whether it succeeded or failed and give us either the file handle or error information — exactly what Result conveys.
  • On success, greeting_file_result holds an instance of Ok containing a file handle; on failure, an instance of Err containing more information about the kind of error that occurred.

The Rust Programming Language · Ch.9 Error Handling

13

Chapter 9 · 9.2 Recoverable Errors with Result

Handling a Result with match

We need to add to the code in Listing 9.3 to take different actions depending on the value File::open returns. Listing 9.4 shows one way, using a basic tool: the match expression from Chapter 6.

Listing 9.4: Using a match expression to handle the Result variants that might be returned — src/main.rs

use std::fs::File;

 

fn main() {

    let greeting_file_result = File::open("hello.txt");

 

    let greeting_file = match greeting_file_result {

        Ok(file) => file,

        Err(error) => panic!("Problem opening the file: {error:?}"),

    };

}

Remark. Like the Option enum, the Result enum and its variants have been brought into scope by the prelude, so we don’t need to specify Result:: before the Ok and Err variants in the match arms.

The Rust Programming Language · Ch.9 Error Handling

14

Chapter 9 · 9.2 Recoverable Errors with Result

The two arms of the match

  • Ok arm. When the result is Ok, this code returns the inner file value out of the Ok variant, and we assign that file handle to the variable greeting_file. After the match, we can use the file handle for reading or writing.
  • Err arm. It handles the case where we get an Err value from File::open. Here we’ve chosen to call the panic! macro.

If there’s no file named hello.txt in our current directory and we run this code, we’ll see the output below from the panic! macro. As usual, it tells us exactly what has gone wrong.

$ cargo run

   Compiling error-handling v0.1.0 (file:///projects/error-handling)

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.73s

     Running `target/debug/error-handling`

 

thread 'main' (6018048) panicked at src/main.rs:8:23:

Problem opening the file: Os { code: 2, kind: NotFound, message: "No such file or directory" }

note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

The Rust Programming Language · Ch.9 Error Handling

15

Chapter 9 · 9.2 Recoverable Errors with Result

Matching on different errors

Listing 9.4 will panic! no matter why File::open failed. We want different actions for different reasons: if the file doesn’t exist, create it and return the handle to the new file; for any other reason (e.g. no permission), still panic! as in Listing 9.4. For this, we add an inner match expression.

Listing 9.5: Handling different kinds of errors in different ways — src/main.rs

use std::fs::File;

use std::io::ErrorKind;

 

fn main() {

    let greeting_file_result = File::open("hello.txt");

 

    let greeting_file = match greeting_file_result {

        Ok(file) => file,

        Err(error) => match error.kind() {

            ErrorKind::NotFound => match File::create("hello.txt") {

                Ok(fc) => fc,

The Rust Programming Language · Ch.9 Error Handling

16

Chapter 9 · 9.2 Recoverable Errors with Result

Matching on different errors (continued)

Listing 9.5 (continued)

                Err(e) => panic!("Problem creating the file: {e:?}"),

            },

            _ => {

                panic!("Problem opening the file: {error:?}");

            }

        },

    };

}

  • The value File::open returns inside the Err variant is an io::Error, a struct provided by the standard library. Its method kind gives an io::ErrorKind value.
  • The standard-library enum io::ErrorKind has variants representing the different kinds of errors an io operation might produce. We want ErrorKind::NotFound: the file we’re trying to open doesn’t exist yet.
  • So we match on greeting_file_result, but we also have an inner match on error.kind().

The Rust Programming Language · Ch.9 Error Handling

17

Chapter 9 · 9.2 Recoverable Errors with Result

The inner match, and alternatives to match

The inner match checks whether the value returned by error.kind() is the NotFound variant of the ErrorKind enum. If it is, we try to create the file with File::create. However, because File::create could also fail, we need a second arm in the inner match: when the file can’t be created, a different error message is printed. The second arm of the outer match stays the same, so the program panics on any error besides the missing file error.

Alternatives to using match with Result<T, E>

That’s a lot of match! The match expression is very useful but also very much a primitive. In Chapter 13, you’ll learn about closures, which are used with many of the methods defined on Result<T, E>. These methods can be more concise than using match when handling Result<T, E> values in your code.

For example, the next slide writes the same logic as Listing 9.5, this time using closures and the unwrap_or_else method.

The Rust Programming Language · Ch.9 Error Handling

18

Chapter 9 · 9.2 Recoverable Errors with Result

The same logic with unwrap_or_else and closures

use std::fs::File;

use std::io::ErrorKind;

 

fn main() {

    let greeting_file = File::open("hello.txt").unwrap_or_else(|error| {

        if error.kind() == ErrorKind::NotFound {

            File::create("hello.txt").unwrap_or_else(|error| {

                panic!("Problem creating the file: {error:?}");

            })

        } else {

            panic!("Problem opening the file: {error:?}");

        }

    });

}

This code has the same behavior as Listing 9.5, but it doesn’t contain any match expressions and is cleaner to read. Come back to it after Chapter 13 and look up unwrap_or_else in the standard library documentation. Many more of these methods can clean up huge, nested match expressions when you’re dealing with errors.

The Rust Programming Language · Ch.9 Error Handling

19

Chapter 9 · 9.2 Recoverable Errors with Result

Shortcuts for panic on error: unwrap

Using match works well enough, but it can be a bit verbose and doesn’t always communicate intent well. Result<T, E> has many helper methods for more specific tasks. The unwrap method is a shortcut implemented just like the match in Listing 9.4: if the Result is Ok, unwrap returns the value inside the Ok; if it is Err, unwrap calls the panic! macro for us.

use std::fs::File;

 

fn main() {

    let greeting_file = File::open("hello.txt").unwrap();

}

Run without a hello.txt file, it shows the error message from the panic! call that unwrap makes:

thread 'main' panicked at src/main.rs:4:49:

called `Result::unwrap()` on an `Err` value:

    Os { code: 2, kind: NotFound, message: "No such file or directory" }

The Rust Programming Language · Ch.9 Error Handling

20

Chapter 9 · 9.2 Recoverable Errors with Result

Shortcuts for panic on error: expect

Similarly, expect also lets us choose the panic! message. Used instead of unwrap with a good message, it conveys your intent and makes tracking down the source of a panic easier:

use std::fs::File;

 

fn main() {

    let greeting_file = File::open("hello.txt")

        .expect("hello.txt should be included in this project");

}

We use expect like unwrap: to return the file handle or call panic!. The message expect uses in its call to panic! is the parameter we pass to it, rather than the default message unwrap uses:

thread 'main' panicked at src/main.rs:5:10:

hello.txt should be included in this project:

    Os { code: 2, kind: NotFound, message: "No such file or directory" }

The Rust Programming Language · Ch.9 Error Handling

21

Chapter 9 · 9.2 Recoverable Errors with Result

expect in production code

In production-quality code, most Rustaceans choose expect rather than unwrap and give more context about why the operation is expected to always succeed. That way, if your assumptions are ever proven wrong, you have more information to use in debugging.

Table 9.2: Ways to handle a Result<T, E> seen so far

ApproachOn Ok(value)On Err(error)
match (Listing 9.4)whatever the Ok arm doeswhatever the Err arm does
unwrap_or_else(closure)returns the valueruns the closure on the error
unwrap()returns the valuecalls panic! with a default message
expect(msg)returns the valuecalls panic! with msg, then the error

The Rust Programming Language · Ch.9 Error Handling

22

Chapter 9 · 9.2 Recoverable Errors with Result

Propagating errors

When a function’s implementation calls something that might fail, instead of handling the error within the function itself, you can return the error to the calling code so that it can decide what to do.

Definition 9.8 (Propagating an error). Returning an error to the calling code rather than handling it in place. It gives more control to the calling code, where there might be more information or logic that dictates how the error should be handled than what is available in the context of your code.

For example, Listing 9.6 (next slides) shows a function that reads a username from a file. If the file doesn’t exist or can’t be read, this function returns those errors to the code that called it.

Remark. This function can be written in a much shorter way, but the book starts by doing a lot of it manually in order to explore error handling; the shorter way comes at the end.

The Rust Programming Language · Ch.9 Error Handling

23

Chapter 9 · 9.2 Recoverable Errors with Result

Propagating errors with match

Listing 9.6: A function that returns errors to the calling code using match — src/main.rs

use std::fs::File;

use std::io::{self, Read};

 

fn read_username_from_file() -> Result<String, io::Error> {

    let username_file_result = File::open("hello.txt");

 

    let mut username_file = match username_file_result {

        Ok(file) => file,

        Err(e) => return Err(e),

    };

Look at the return type first: Result<String, io::Error>. The function returns a value of type Result<T, E>, where the generic parameter T has been filled in with the concrete type String and the generic type E with the concrete type io::Error.

The Rust Programming Language · Ch.9 Error Handling

24

Chapter 9 · 9.2 Recoverable Errors with Result

Propagating errors with match (continued)

Listing 9.6 (continued)

    let mut username = String::new();

 

    match username_file.read_to_string(&mut username) {

        Ok(_) => Ok(username),

        Err(e) => Err(e),

    }

}

  • If this function succeeds without any problems, the calling code receives an Ok value that holds a String — the username that this function read from the file.
  • If it encounters any problems, the calling code receives an Err value that holds an instance of io::Error containing more information about what the problems were.
  • We chose io::Error as the error type because it happens to be the type of the error value returned by both fallible operations in the body: the File::open function and the read_to_string method.

The Rust Programming Language · Ch.9 Error Handling

25

Chapter 9 · 9.2 Recoverable Errors with Result

Walking through Listing 9.6

  1. The body starts by calling File::open, then handles the Result with a match similar to the one in Listing 9.4. If File::open succeeds, the file handle in the pattern variable file becomes the value in the mutable variable username_file, and the function continues.
  2. In the Err case, instead of calling panic!, we use the return keyword to return early out of the function entirely, passing the error value from File::open (now in the pattern variable e) back to the calling code as this function’s error value.
  3. With a file handle in username_file, the function creates a new String in username and calls read_to_string on the file handle to read the file’s contents into username.
  4. read_to_string also returns a Result, because it might fail even though File::open succeeded — so another match handles it. On success, our function has succeeded and returns the username wrapped in an Ok. On failure, it returns the error value just as for File::open. No explicit return is needed, because this is the last expression in the function.

The Rust Programming Language · Ch.9 Error Handling

26

Chapter 9 · 9.2 Recoverable Errors with Result

The caller decides

The code that calls read_username_from_file will then handle getting either an Ok value that contains a username or an Err value that contains an io::Error. It’s up to the calling code to decide what to do with those values. If it gets an Err value, it could, for example:

  • call panic! and crash the program,
  • use a default username, or
  • look up the username from somewhere other than a file.

We don’t have enough information on what the calling code is actually trying to do, so we propagate all the success or error information upward for it to handle appropriately.

This pattern of propagating errors is so common in Rust that Rust provides the question mark operator ? to make it easier.

The Rust Programming Language · Ch.9 Error Handling

27

Chapter 9 · 9.2 Recoverable Errors with Result

The ? operator shortcut

Listing 9.7 has the same functionality as Listing 9.6, but uses the ? operator.

Listing 9.7: A function that returns errors to the calling code using the ? operator — src/main.rs

use std::fs::File;

use std::io::{self, Read};

 

fn read_username_from_file() -> Result<String, io::Error> {

    let mut username_file = File::open("hello.txt")?;

    let mut username = String::new();

    username_file.read_to_string(&mut username)?;

    Ok(username)

}

Definition 9.9 (The ? operator). Placed after a Result value, it works almost like the match of Listing 9.6: on Ok, the value inside is the expression’s value and the program continues; on Err, the Err is returned from the whole function as if by return, propagating the error to the calling code.

The Rust Programming Language · Ch.9 Error Handling

28

Chapter 9 · 9.2 Recoverable Errors with Result

How ? differs from match: error conversion with From

There is one difference between the match of Listing 9.6 and the ? operator. Error values that ? is called on go through the from function, defined in the From trait in the standard library, which converts values from one type into another. When ? calls from, the error type received is converted into the error type defined in the return type of the current function. This is useful when a function returns one error type to represent all the ways it might fail, even if parts might fail for many different reasons.

Example 9.10. We could change read_username_from_file in Listing 9.7 to return a custom error type named OurError that we define. If we also define impl From<io::Error> for OurError to construct an OurError from an io::Error, then the ? calls in the body will call from and convert the error types without needing to add any more code to the function.

In Listing 9.7, the ? at the end of the File::open call returns the value inside an Ok to the variable username_file. If an error occurs, ? returns early out of the whole function and gives any Err value to the calling code. The same applies to the ? at the end of the read_to_string call.

The Rust Programming Language · Ch.9 Error Handling

29

Chapter 9 · 9.2 Recoverable Errors with Result

Chaining method calls after ?

? eliminates a lot of boilerplate; we can shorten further by chaining calls right after the ?:

Listing 9.8: Chaining method calls after the ? operator — src/main.rs

use std::fs::File;

use std::io::{self, Read};

 

fn read_username_from_file() -> Result<String, io::Error> {

    let mut username = String::new();

 

    File::open("hello.txt")?.read_to_string(&mut username)?;

 

    Ok(username)

}

The new String moved to the start. Instead of a variable username_file, we chain read_to_string directly onto File::open("hello.txt")?, keep its ?, and still return Ok(username) when both succeed: the same functionality as Listings 9.6 and 9.7, just more ergonomic.

The Rust Programming Language · Ch.9 Error Handling

30

Chapter 9 · 9.2 Recoverable Errors with Result

Even shorter: fs::read_to_string

Listing 9.9 shows a way to make this even shorter.

Listing 9.9: Using fs::read_to_string instead of opening and then reading the file — src/main.rs

use std::fs;

use std::io;

 

fn read_username_from_file() -> Result<String, io::Error> {

    fs::read_to_string("hello.txt")

}

Reading a file into a string is a fairly common operation, so the standard library provides the convenient fs::read_to_string function. It opens the file, creates a new String, reads the contents of the file, puts the contents into that String, and returns it.

Remark. Using fs::read_to_string doesn’t give us the opportunity to explain all the error handling, which is why the book did it the longer way first.

The Rust Programming Language · Ch.9 Error Handling

31

Chapter 9 · 9.2 Recoverable Errors with Result

Where the ? operator can be used

The ? operator can only be used in functions whose return type is compatible with the value the ? is used on. This is because ? is defined to perform an early return of a value out of the function, in the same manner as the match of Listing 9.6. There, the match was using a Result value, and the early return arm returned an Err(e) value. The return type of the function has to be a Result so that it’s compatible with this return.

Listing 9.10 uses ? in a main function whose return type is incompatible with the type of the value we use ? on.

Listing 9.10: Attempting to use the ? in the main function that returns () won’t compile. — src/main.rs

use std::fs::File;

 

fn main() {

    let greeting_file = File::open("hello.txt")?;

}

The Rust Programming Language · Ch.9 Error Handling

32

Chapter 9 · 9.2 Recoverable Errors with Result

Error E0277: ? in a function that returns ()

This code opens a file, which might fail. The ? operator follows the Result value returned by File::open, but this main function has the return type (), not Result. Compiling it gives this error:

$ cargo run

   Compiling error-handling v0.1.0 (file:///projects/error-handling)

error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option`

    (or another type that implements `FromResidual`)

 --> src/main.rs:4:48

  |

3 | fn main() {

  | --------- this function should return `Result` or `Option` to accept `?`

4 |     let greeting_file = File::open("hello.txt")?;

  |                                                ^ cannot use the `?` operator in a function

  |                                                  that returns `()`

  |

help: consider adding return type

The Rust Programming Language · Ch.9 Error Handling

33

Chapter 9 · 9.2 Recoverable Errors with Result

Error E0277 (continued), and two ways to fix it

  |

3 ~ fn main() -> Result<(), Box<dyn std::error::Error>> {

4 |     let greeting_file = File::open("hello.txt")?;

5 +     Ok(())

  |

 

For more information about this error, try `rustc --explain E0277`.

error: could not compile `error-handling` (bin "error-handling") due to 1 previous error

This error points out that we’re only allowed to use the ? operator in a function that returns Result, Option, or another type that implements FromResidual. To fix the error, you have two choices:

  1. Change the return type of your function to be compatible with the value you’re using the ? operator on, as long as you have no restrictions preventing that.
  2. Use a match or one of the Result<T, E> methods to handle the Result<T, E> in whatever way is appropriate.

The Rust Programming Language · Ch.9 Error Handling

34

Chapter 9 · 9.2 Recoverable Errors with Result

Using ? on Option<T> values

The error message also mentioned that ? can be used with Option<T> values as well. As with using ? on Result, you can only use ? on Option in a function that returns an Option. The behavior of ? on an Option<T> is similar to its behavior on a Result<T, E>:

Table 9.3: What the ? operator does with each value

ValueEffect of ?
Ok(v)v is the resultant value of the expression; the function continues
Err(e)Err is returned early from the function (after conversion by From::from)
Some(v)v is the resultant value of the expression; the function continues
NoneNone is returned early from the function at that point

Listing 9.11 (next slide) has an example: a function that finds the last character of the first line in the given text.

The Rust Programming Language · Ch.9 Error Handling

35

Chapter 9 · 9.2 Recoverable Errors with Result

The last character of the first line

Listing 9.11: Using the ? operator on an Option<T> value

fn last_char_of_first_line(text: &str) -> Option<char> {

    text.lines().next()?.chars().last()

}

 

fn main() {

    assert_eq!(

        last_char_of_first_line("Hello, world\nHow are you today?"),

        Some('d')

    );

 

    assert_eq!(last_char_of_first_line(""), None);

    assert_eq!(last_char_of_first_line("\nhi"), None);

}

The function returns Option<char> because it’s possible that there is a character there, but it’s also possible that there isn’t.

The Rust Programming Language · Ch.9 Error Handling

36

Chapter 9 · 9.2 Recoverable Errors with Result

How Listing 9.11 works

  1. The code takes the text string slice argument and calls the lines method on it, which returns an iterator over the lines in the string.
  2. Because the function wants to examine the first line, it calls next on the iterator to get its first value. If text is the empty string, next returns None, and we use ? to stop and return None from last_char_of_first_line. Otherwise next returns a Some value containing a string slice of the first line.
  3. The ? extracts the string slice, and we call chars on it to get an iterator of its characters, then last to return the last item in the iterator.
  4. last returns an Option, because the first line may be the empty string — for example, if text starts with a blank line but has characters on other lines, as in "\nhi". If there is a last character on the first line, it is returned in the Some variant.

The ? in the middle gives a concise way to express this logic, implementing the function in one line. Without ? on Option, we’d need more method calls or a match expression.

The Rust Programming Language · Ch.9 Error Handling

37

Chapter 9 · 9.2 Recoverable Errors with Result

No mixing Result and Option; what main may return

You can’t mix and match

You can use ? on a Result in a function that returns Result, and on an Option in a function that returns Option, but you can’t mix and match. The ? operator won’t automatically convert a Result to an Option or vice versa; in those cases, use methods like ok on Result or ok_or on Option to do the conversion explicitly.

The return type of main

So far, all the main functions we’ve used return (). The main function is special because it’s the entry point and exit point of an executable program, and there are restrictions on what its return type can be for the program to behave as expected.

Luckily, main can also return a Result<(), E>. Listing 9.12 has the code from Listing 9.10, but with the return type of main changed to Result<(), Box<dyn Error>> and a return value Ok(()) added at the end. This code will now compile.

The Rust Programming Language · Ch.9 Error Handling

38

Chapter 9 · 9.2 Recoverable Errors with Result

main returning Result<(), Box<dyn Error>>

Listing 9.12: Changing main to return Result<(), E> allows the use of the ? operator on Result values. — src/main.rs

use std::error::Error;

use std::fs::File;

 

fn main() -> Result<(), Box<dyn Error>> {

    let greeting_file = File::open("hello.txt")?;

 

    Ok(())

}

  • The Box<dyn Error> type is a trait object, covered in Chapter 18 (“Using Trait Objects to Abstract over Shared Behavior”). For now, read Box<dyn Error> as “any kind of error.”
  • Using ? on a Result in a main with error type Box<dyn Error> is allowed because it allows any Err value to be returned early.
  • Even though this body only ever returns errors of type std::io::Error, specifying Box<dyn Error> keeps the signature correct even if code returning other errors is added to main.

The Rust Programming Language · Ch.9 Error Handling

39

Chapter 9 · 9.2 Recoverable Errors with Result

Exit codes and the Termination trait

When a main function returns a Result<(), E>, the executable exits with a value of 0 if main returns Ok(()), and with a nonzero value if main returns an Err value.

Executables written in C return integers when they exit: programs that exit successfully return the integer 0, and programs that error return some integer other than 0. Rust also returns integers from executables to be compatible with this convention.

The main function may return any types that implement the std::process::Termination trait, which contains a function report that returns an ExitCode. Consult the standard library documentation for more information on implementing the Termination trait for your own types.

Remark. Now that we’ve discussed the details of calling panic! or returning Result, the next section returns to how to decide which is appropriate to use in which cases.

The Rust Programming Language · Ch.9 Error Handling

40

Chapter 9 · 9.3 To panic! or Not to panic!

panic! or Result: returning Result is the default

So, how do you decide when you should call panic! and when you should return Result?

  • When code panics, there’s no way to recover. You could call panic! for any error situation, whether there’s a possible way to recover or not — but then you’re making the decision that a situation is unrecoverable on behalf of the calling code.
  • When you choose to return a Result value, you give the calling code options. It could attempt to recover in a way that’s appropriate for its situation, or it could decide that an Err value in this case is unrecoverable, call panic!, and turn your recoverable error into an unrecoverable one.

Therefore, returning Result is a good default choice when you’re defining a function that might fail.

In situations such as examples, prototype code, and tests, it’s more appropriate to write code that panics instead of returning a Result. This section explores why, then discusses situations in which the compiler can’t tell that failure is impossible but you as a human can, and concludes with general guidelines on whether to panic in library code.

The Rust Programming Language · Ch.9 Error Handling

41

Chapter 9 · 9.3 To panic! or Not to panic!

Examples, prototype code, and tests

Examples

When you’re writing an example to illustrate some concept, also including robust error-handling code can make the example less clear. In examples, it’s understood that a call to a method like unwrap that could panic is meant as a placeholder for the way you’d want your application to handle errors, which can differ based on what the rest of your code is doing.

Prototypes

The unwrap and expect methods are very handy when you’re prototyping and not yet ready to decide how to handle errors. They leave clear markers in your code for when you’re ready to make your program more robust.

Tests

If a method call fails in a test, you’d want the whole test to fail, even if that method isn’t the functionality under test. Because panic! is how a test is marked as a failure, calling unwrap or expect is exactly what should happen.

The Rust Programming Language · Ch.9 Error Handling

42

Chapter 9 · 9.3 To panic! or Not to panic!

When you have more information than the compiler

It is also appropriate to call expect when some other logic ensures that the Result will have an Ok value, but the logic isn’t something the compiler understands. You still have a Result to handle: whatever operation you’re calling can still fail in general, even though it’s logically impossible in your particular situation. If you can ensure by manually inspecting the code that you’ll never have an Err variant, it’s perfectly acceptable to call expect and document in the argument text the reason you think you’ll never have one.

Example 9.11. Creating an IpAddr instance by parsing a hardcoded string:

fn main() {

    use std::net::IpAddr;

 

    let home: IpAddr = "127.0.0.1"

        .parse()

        .expect("Hardcoded IP address should be valid");

}

The Rust Programming Language · Ch.9 Error Handling

43

Chapter 9 · 9.3 To panic! or Not to panic!

Why expect is acceptable in Example 9.11

  • We can see that 127.0.0.1 is a valid IP address, so it’s acceptable to use expect here.
  • However, having a hardcoded, valid string doesn’t change the return type of the parse method: we still get a Result value, and the compiler will still make us handle the Result as if the Err variant is a possibility, because the compiler isn’t smart enough to see that this string is always a valid IP address.
  • If the IP address string came from a user rather than being hardcoded into the program, and therefore did have a possibility of failure, we’d definitely want to handle the Result in a more robust way instead.
  • Mentioning the assumption that this IP address is hardcoded will prompt us to change expect to better error-handling code if, in the future, we need to get the IP address from some other source instead.

The Rust Programming Language · Ch.9 Error Handling

44

Chapter 9 · 9.3 To panic! or Not to panic!

Guidelines for error handling: the bad state

It’s advisable to have your code panic when it’s possible that your code could end up in a bad state.

Definition 9.12 (Bad state). A state in which some assumption, guarantee, contract, or invariant has been broken — such as when invalid values, contradictory values, or missing values are passed to your code — plus one or more of the conditions below.

  • The bad state is something that is unexpected, as opposed to something that will likely happen occasionally, like a user entering data in the wrong format.
  • Your code after this point needs to rely on not being in this bad state, rather than checking for the problem at every step.
  • There’s not a good way to encode this information in the types you use. Chapter 18 works through an example in “Encoding States and Behavior as Types”.

The Rust Programming Language · Ch.9 Error Handling

45

Chapter 9 · 9.3 To panic! or Not to panic!

Values that don’t make sense, and expected failure

When to panic

If someone calls your code and passes in values that don’t make sense, it’s best to return an error if you can, so that the user of the library can decide what they want to do in that case.

However, where continuing could be insecure or harmful, the best choice might be to call panic! and alert the person using your library to the bug in their code so they can fix it during development.

Similarly, panic! is often appropriate if you’re calling external code that is out of your control and returns an invalid state that you have no way of fixing.

When to return Result

When failure is expected, it’s more appropriate to return a Result than to make a panic! call. Examples:

  • a parser being given malformed data;
  • an HTTP request returning a status that indicates you have hit a rate limit.

Returning a Result indicates that failure is an expected possibility that the calling code must decide how to handle.

The Rust Programming Language · Ch.9 Error Handling

46

Chapter 9 · 9.3 To panic! or Not to panic!

Invalid values, safety, and contracts

When your code performs an operation that could put a user at risk if it’s called using invalid values, your code should verify the values are valid first and panic if they aren’t. This is mostly for safety reasons: attempting to operate on invalid data can expose your code to vulnerabilities.

This is the main reason the standard library will call panic! if you attempt an out-of-bounds memory access: trying to access memory that doesn’t belong to the current data structure is a common security problem.

Definition 9.13 (Contract). A function’s requirement on its inputs: its 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’s not a kind of error you want the calling code 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.

The Rust Programming Language · Ch.9 Error Handling

47

Chapter 9 · 9.3 To panic! or Not to panic!

Letting the type system do the checks

Having lots of error checks in all of your functions would be verbose and annoying. Fortunately, you can use Rust’s type system (and thus the type checking done by the compiler) to do many of the checks for you. If your function has a particular type as a parameter, you can proceed with your code’s logic knowing that the compiler has already ensured that you have a valid value.

Example 9.14. If you have a type rather than an Option, your program expects to have something rather than nothing. Your code then doesn’t have to handle two cases for the Some and None variants: it has only one case, for definitely having a value. Code trying to pass nothing to your function won’t even compile, so your function doesn’t have to check for that case at runtime.

Example 9.15. Using an unsigned integer type such as u32 ensures that the parameter is never negative.

The Rust Programming Language · Ch.9 Error Handling

48

Chapter 9 · 9.3 To panic! or Not to panic!

Custom types for validation: the problem

Let’s take the idea of using Rust’s type system to ensure we have a valid value one step further, and create a custom type for validation.

Recall the guessing game in Chapter 2, which asked the user to guess a number between 1 and 100. We never validated that the guess was between those numbers before checking it against our secret number; we only validated that the guess was positive. The consequences were not very dire: our output of “Too high” or “Too low” would still be correct.

But it would be a useful enhancement to guide the user toward valid guesses, and to behave differently when the user guesses a number that’s out of range versus when the user types, for example, letters instead.

One way to do this would be to parse the guess as an i32 instead of only a u32 to allow potentially negative numbers, and then add a check for the number being in range, as on the next slides.

The Rust Programming Language · Ch.9 Error Handling

49

Chapter 9 · 9.3 To panic! or Not to panic!

A range check in the guessing game (1/3)

src/main.rs

use std::cmp::Ordering;

use std::io;

 

use rand::prelude::*;

 

fn main() {

    println!("Guess the number!");

 

    let secret_number = rand::rng().random_range(1..=100);

 

    loop {

        // --snip--

 

        println!("Please input your guess.");

 

        let mut guess = String::new();

The Rust Programming Language · Ch.9 Error Handling

50

Chapter 9 · 9.3 To panic! or Not to panic!

A range check in the guessing game (2/3)

src/main.rs (continued)

        io::stdin()

            .read_line(&mut guess)

            .expect("Failed to read line");

 

        let guess: i32 = 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;

        }

The if expression checks whether 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.

The Rust Programming Language · Ch.9 Error Handling

51

Chapter 9 · 9.3 To panic! or Not to panic!

A range check in the guessing game (3/3)

src/main.rs (continued)

        match guess.cmp(&secret_number) {

            // --snip--

            Ordering::Less => println!("Too small!"),

            Ordering::Greater => println!("Too big!"),

            Ordering::Equal => {

                println!("You win!");

                break;

            }

        }

    }

}

After the if, we compare guess with the secret number knowing that guess is between 1 and 100.

This is not ideal: if it were absolutely critical that the program only operated on values between 1 and 100, and many functions had this requirement, a check like this in every function would be tedious (and might impact performance).

The Rust Programming Language · Ch.9 Error Handling

52

Chapter 9 · 9.3 To panic! or Not to panic!

A Guess type that validates on creation

Instead, make a new type in a dedicated module and put the validations in a function that creates an instance of the type, rather than repeating them everywhere. Functions can then safely use the new type in their signatures and confidently use the values they receive.

Listing 9.13: A Guess type that will only continue with values between 1 and 100 — src/guessing_game.rs

pub struct Guess {

    value: i32,

}

 

impl Guess {

    pub fn new(value: i32) -> Guess {

        if value < 1 || value > 100 {

            panic!("Guess value must be between 1 and 100, got {value}.");

        }

 

        Guess { value }

    }

The Rust Programming Language · Ch.9 Error Handling

53

Chapter 9 · 9.3 To panic! or Not to panic!

A Guess type that validates on creation (continued)

Listing 9.13 (continued)

 

    pub fn value(&self) -> i32 {

        self.value

    }

}

Remark. This code in src/guessing_game.rs depends on adding a module declaration mod guessing_game; in src/lib.rs, which isn’t shown here.

Within this new module’s file, we define a struct named Guess that has a field named value that holds an i32. This is where the number will be stored.

The impl Guess block then provides two things, explained on the next slides: an associated function new that creates validated instances, and a method value that reads the stored number.

The Rust Programming Language · Ch.9 Error Handling

54

Chapter 9 · 9.3 To panic! or Not to panic!

Guess::new enforces the contract

We implement an associated function named new on Guess that creates instances of Guess values. It has one parameter named value of type i32 and returns a Guess.

  • The body of new tests value to make sure it’s between 1 and 100.
  • If value doesn’t pass this test, we make a panic! call. It alerts the programmer writing the calling code that they have a bug to fix, because creating a Guess with a value outside this range would violate the contract that Guess::new is relying on.
  • The conditions in which Guess::new might panic should be discussed in its public-facing API documentation; Chapter 14 covers documentation conventions indicating the possibility of a panic!.
  • If value does pass the test, we create a new Guess with its value field set to the value parameter and return the Guess.

The Rust Programming Language · Ch.9 Error Handling

55

Chapter 9 · 9.3 To panic! or Not to panic!

The value getter and the private field

Next, we implement a method named value that borrows self, doesn’t have any other parameters, and returns an i32.

Definition 9.16 (Getter). A method whose purpose is to get some data from its fields and return it.

  • This public method is necessary because the value field of the Guess struct is private.
  • It’s important that the field be private, so that code using the Guess struct is not allowed to set value directly: code outside the guessing_game module must use Guess::new to create an instance of Guess.
  • That ensures there’s no way for a Guess to have a value that hasn’t been checked by the conditions in Guess::new.

A function that has a parameter or returns only numbers between 1 and 100 could then declare in its signature that it takes or returns a Guess rather than an i32, and wouldn’t need to do any additional checks in its body.

The Rust Programming Language · Ch.9 Error Handling

56

Chapter 9 · Summary

Chapter 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 to indicate that operations might fail in a way that your code could recover from.
  • You can use Result to tell code that calls your code that it needs to handle potential success or failure as well.

Using panic! and Result in the appropriate situations will make your code more reliable in the face of inevitable problems.

Remark. Now that you’ve seen useful ways that the standard library uses generics with the Option and Result enums, Chapter 10 talks about how generics work and how you can use them in your code.

The Rust Programming Language · Ch.9 Error Handling

57