Rust groups errors into two major categories: recoverable and unrecoverable errors.

Rust doesn’t have exceptions. Instead, it has the type Result<T, E> for recoverable errors and the panic! macro that stops execution when the program encounters an unrecoverable error.

Panic

By default, panic will print a failure message, unwind, clean up the stack, and quit. Unwind means that Rust will walk back up the stack and clean up the data from each function it encounters.

Unwind is a lot of work. Rust allows us to configure the program to abort, which ends the program without cleaning up - it will be handled by the OS. Add panic='abort' to the appropriate [profile] sections in your Cargo.toml file, e.g:

[profile.release]
panic = 'abort'

Result

Most errors aren’t serious enough to require the program to stop entirely.

enum Result<T, E> {
    Ok(T),
    Err(E),
}

Matching errors:

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,
                Err(e) => panic!("Problem creating the file: {:?}", e),
            },
            other_error => {
                panic!("Problem opening the file: {:?}", other_error);
            }
        },
    };
}
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);
        }
    });
}

The unwrap method is a shortcut method that will panic on error:

use std::fs::File;
 
fn main() {
    let greeting_file = File::open("hello.txt").unwrap();
}

expect works similarly but lets us choose the error message:

use std::fs::File;
 
fn main() {
    let greeting_file = File::open("hello.txt")
        .expect("hello.txt should be included in this project");
}

In production-quality code, most Rustaceans choose expect rather than unwrap and give more context about why the operation is expected to always succeed.

Error propagation

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

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),
    };
 
    let mut username = String::new();
 
    match username_file.read_to_string(&mut username) {
        Ok(_) => Ok(username),
        Err(e) => Err(e),
    }
}
 
// Same as above
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)
}
 
// Even shorter version with chaining
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)
}
 
// Even shorter with fs::read_to_string
use std::fs;
use std::io;
 
fn read_username_from_file() -> Result<String, io::Error> {
    fs::read_to_string("hello.txt")
}

There is a difference between what the match expression from Listing 9-6 does and what the ? operator does: error values that have the ? operator called on them go through the from function, defined in the From trait in the standard library, which is used to convert values from one type into another. When the ? operator calls the from function, 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 a function might fail, even if parts might fail for many different reasons.

For example, we could change the read_username_from_file function 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 instance of OurError from an io::Error, then the ? operator calls in the body of read_username_from_file will call from and convert the error types without needing to add any more code to the function.

The ? operator can only be used in functions whose return type is compatible with the value the ? is used on (Result, Option, or another type that implements FromResidual).

When to panic!

Panic when there’s no way to recover and you don’t have to give the calling code the decision if it’s recoverable or not.

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.

If you can ensure by manually inspecting the code that you’ll never have an Err variant, it’s perfectly acceptable to call unwrap, and even better to document the reason you think you’ll never have an Err variant in the expect text. Here’s an example:

use std::net::IpAddr;
 
let home: IpAddr = "127.0.0.1"
	.parse()
	.expect("Hardcoded IP address should be valid");

It’s advisable to have your code panic when it’s possible that your code could end up in a bad state. In this context, a bad state is when 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 following:

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

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 the user of the library can decide what they want to do in that case. However, in cases 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 it returns an invalid state that you have no way of fixing.

However, when failure is expected, it’s more appropriate to return a Result than to make a panic! call. Examples include a parser being given malformed data or an HTTP request returning a status that indicates you have hit a rate limit. In these cases, returning a Result indicates that failure is an expected possibility that the calling code must decide how to handle.

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.

However, 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.

You can also create custom types for validation.

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 }
    }
 
    pub fn value(&self) -> i32 {
        self.value
    }
}