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
| Category | Rust mechanism |
|---|---|
| Recoverable | The type Result<T, E> |
| Unrecoverable | The 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
| Approach | On Ok(value) | On Err(error) |
|---|---|---|
| match (Listing 9.4) | whatever the Ok arm does | whatever the Err arm does |
| unwrap_or_else(closure) | returns the value | runs the closure on the error |
| unwrap() | returns the value | calls panic! with a default message |
| expect(msg) | returns the value | calls 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
- 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.
- 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.
- 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.
- 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:
- 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.
- 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
| Value | Effect 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 |
| None | None 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
- 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.
- 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.
- 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.
- 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