There are two different kinds of error handling in Rust: panic and Results.

  1. Ordinary errors are handled using the Result type.
    • Results typically represent problems caused by things outside the program, like erroneous input, a network outage, or a permissions problem.
  2. Panic is for the kind of error that should never happen.

Panic

A program panics when it encounters something so messed up that there must be a bug in the program itself. Something like:

  • Out-of-bounds array access
  • Integer division by zero
  • Calling .expect() on a Result that happens to be Err
  • Assertion failure

What these conditions have in common is that they are all the programmer’s fault.

When these errors that shouldn’t happen do happen, Rust can either unwind the stack when a panic happens or abort the process. Unwinding is the default.

Unwinding

In Rust, attempting to divide by zero triggers a panic, which typically proceeds as follows:

  • An error message is printed to the terminal.
    • If you set the RUST_BACKTRACE environment variable, Rust will also dump the stack at this point
  • The stack is unwound.
    • Any temporary values, local variables, or arguments that the current function was using are dropped, in the reverse of the order they were created. Dropping a value simply means cleaning up after it: any Strings or Vecs the program was using are freed, any open Files are closed, and so on. User-defined drop methods are called too.
    • Once the current function call is cleaned up, we move on to its caller, dropping its variables and arguments the same way. Then we move to that function’s caller, and so on up the stack.
  • The thread exits.
    • If the panicking thread was the main thread, then the whole process exits (with a nonzero exit code).

A panic is not a crash. It’s not undefined behavior. It’s more like a RuntimeException in Java or a std::logic_error in C++. The behavior is well-defined; it just shouldn’t be happening.

Panic is safe. It doesn’t violate any of Rust’s safety rules; even if you manage to panic in the middle of a standard library method, it will never leave a dangling pointer or a half-initialized value in memory. Panic is per thread. One thread can be panicking while other threads are going on about their normal business. The idea is that Rust catches the invalid array access, or whatever it is, before anything bad happens. It would be unsafe to proceed, so Rust unwinds the stack. But the rest of the process can continue running.

The standard library function std::panic::catch_unwind() catches stack unwinding, allowing the thread to survive and continue running.

  • This is the mechanism used by Rust’s test harness to recover when an assertion fails in a test.
  • It can also be necessary when writing Rust code that can be called from C or C++, because unwinding across non-Rust code is undefined behavior.

You can use threads and catch_unwind() to handle panic, making your program more robust. One important caveat is that these tools only catch panics that unwind the stack. Not every panic proceeds this way.

Aborting

Stack unwinding is the default panic behavior, but there are two circumstances in which Rust does not try to unwind the stack.

  1. If a .drop() method triggers a second panic while Rust is still trying to clean up after the first, this is considered fatal. Rust stops unwinding and aborts the whole process.
  2. Rust’s panic behavior is customizable. If you compile with -C panic=abort, the first panic in your program immediately aborts the process.
    • With this option, Rust does not need to know how to unwind the stack, so this can reduce the size of your compiled code.

Result

Rust doesn’t have exceptions. Instead, functions that can fail have a return type that says so:

fn get_weather(location: LatLng) -> Result<WeatherReport, io::Error>
  • The Result type indicates possible failure. When we call the get_weather() function, it will return either a success result Ok(weather), where weather is a new WeatherReport value, or an error result Err(error_value), where error_value is an io::Error explaining what went wrong.
  • Rust requires us to write some kind of error handling whenever we call this function. We can’t get at the WeatherReport without doing something to the Result, and you’ll get a compiler warning if a Result value isn’t used.

Catching Errors

The most thorough way of dealing with a Result is using a match expression. This is Rust’s equivalent of try/catch in other languages. It’s what you use when you want to handle errors head-on, not pass them on to your caller.

match get_weather(hometown) {
    Ok(report) => {
        display_weather(hometown, &report);
    }
    Err(err) => {
        println!("error querying the weather: {}", err);
        schedule_weather_retry();
    }
}

match is a bit verbose, so Result<T, E> offers a variety of methods that are useful in particular common cases. Each of these methods has a match expression in its implementation.

  • result.is_ok(), result.is_err()

    • Return a bool telling if result is a success result or an error result.
  • result.ok()

    • Returns the success value, if any, as an Option<T>. If result is a success result, this returns Some(success_value); otherwise, it returns None, discarding the error value.
  • result.err()

    • Returns the error value, if any, as an Option<E>.
  • result.unwrap_or(fallback)

    • Returns the success value, if result is a success result. Otherwise, it returns fallback value, discarding the error value. This is a nice alternative to .ok() because the return type is T, not Option<T>. It works only when there’s an appropriate fallback value.

      // A fairly safe prediction for Southern California.
      const THE_USUAL: WeatherReport = WeatherReport::Sunny(72);
      
      // Get a real weather report, if possible.
      // If not, fall back on the usual.
      let report = get_weather(los_angeles).unwrap_or(THE_USUAL);
      display_weather(los_angeles, &report);
      
  • result.unwrap_or_else(fallback_fn)

    • This is the same, but instead of passing a fallback value directly, you pass a function or closure. This is for cases where it would be wasteful to compute a fallback value if you’re not going to use it. The fallback_fn is called only if we have an error result.

      let report =
          get_weather(hometown)
          .unwrap_or_else(|_err| vague_prediction(hometown));
      
  • result.unwrap()

    • Returns the success value, if result is a success result. However, if result is an error result, this method panics.
  • result.expect(message)

    • This the same as .unwrap(), but lets you provide a message that it prints in case of panic.
  • result.as_ref()

    • Converts a Result<T, E> to a Result<&T, &E>.
  • result.as_mut()

    • This is the same, but borrows a mutable reference. The return type is Result<&mut T, &mut E>.

All of the other methods listed here, except .is_ok() and .is_err(), consume the result they operate on. That is, they take the self argument by value. Sometimes it’s quite handy to access data inside a result without destroying it, and this is what .as_ref() and .as_mut() do for us.

  • For example, suppose you’d like to call result.ok(), but you need result to be left intact. You can write result.as_ref().ok(), which merely borrows result, returning an Option<&T> rather than an Option<T>.

Result Type Aliases

A type alias is a kind of shorthand for type names. Modules often define a Result type alias to avoid having to repeat an error type that’s used consistently by almost every function in the module.

For example, the standard library’s std::io module includes this line of code:

pub type Result<T> = result::Result<T, Error>;
  • This defines a public type std::io::Result<T>. It’s an alias for Result<T, E>, but hardcodes std::io::Error as the error type. In practical terms, this means that if you write use std::io;, then Rust will understand io::Result<String> as shorthand for Result<String, io::Error>.

Printing Errors

Sometimes the only way to handle an error is by dumping it to the terminal and moving on.

The standard library defines several error types with names: std::io::Error, std::fmt::Error, std::str::Utf8Error, and so on. All of them implement a common interface, the std::error::Error trait, which means they share the following features and methods:

  • println!()

    • All error types are printable using this. Printing an error with the {} format specifier typically displays only a brief error message. Alternatively, you can print with the {:?} format specifier, to get a Debug view of the error.

      // result of `println!("error: {}", err);`
      error: failed to lookup address information: No address associated with
      hostname
      
      // result of `println!("error: {:?}", err);`
      error: Error { repr: Custom(Custom { kind: Other, error: StringError(
      "failed to lookup address information: No address associated with
      hostname") }) }
      
  • err.to_string()

    • Returns an error message as a String.
  • err.source()

    • Returns an Option of the underlying error, if any, that caused err.
      • For example, a networking error might cause a banking transaction to fail, which could in turn cause your boat to be repossessed. If err.to_string() is "boat was repossessed", then err.source() might return an error about the failed transaction. That error’s .to_string() might be "failed to transfer $300 to United Yacht Supply", and its .source() might be an io::Error with details about the specific network outage that caused all the fuss. This third error is the root cause, so its .source() method would return None.
      • Since the standard library only includes rather low-level features, the source of errors returned from the standard library is usually None.

Printing an error value does not also print out its source. If you want to be sure to print all the available information, use this function:

use std::error::Error;
use std::io::{Write, stderr};

/// Dump an error message to `stderr`.
///
/// If another error happens while building the error message or
/// writing to `stderr`, it is ignored.
fn print_error(mut err: &dyn Error) {
    let _ = writeln!(stderr(), "error: {}", err);
    while let Some(source) = err.source() {
        let _ = writeln!(stderr(), "caused by: {}", source);
        err = source;
    }
}
  • The writeln! macro works like println!, except that it writes the data to a stream.
    • eprintln! does the same thing, but it panics if an error occurs

The standard library’s error types do not include a stack trace, but the popular anyhow crate provides a ready-made error type that does, when used with an unstable version of the Rust compiler. (As of Rust 1.50, the standard library’s functions for capturing backtraces were not yet stabilized.)

Propagating Errors

In most places where we try something that could fail, we don’t want to catch and handle the error immediately. It is simply too much code to use a 10-line match statement every place where something could go wrong. Instead, if an error occurs, we usually want to let our caller deal with it. We want errors to propagate up the call stack.

Rust has a ? operator that does this. You can add a ? to any expression that produces a Result, such as the result of a function call:

let weather = get_weather(hometown)?;
  • On success, it unwraps the Result to get the success value inside. The type of weather here is not Result<WeatherReport, io::Error> but simply WeatherReport.
  • On error, it immediately returns from the enclosing function, passing the error result up the call chain. To ensure that this works, ? can only be used on a Result in functions that have a Result return type.

There’s nothing magical about the ? operator. You can express the same thing using a match expression:

let weather = match get_weather(hometown) {
    Ok(success_value) => success_value,
    Err(err) => return Err(err)
};
  • The only differences between this and the ? operator are some fine points involving types and conversions.

In older code, you may see the try!() macro, which was the usual way to propagate errors until the ? operator was introduced in Rust 1.13:

let weather = try!(get_weather(hometown));
  • The macro expands to a match expression, like the one earlier.

The ? operator sometimes shows up on almost every line of a function:

use std::fs;
use std::io;
use std::path::Path;

// pub type Result<T> = result::Result<T, Error>
fn move_all(src: &Path, dst: &Path) -> io::Result<()> {
    for entry_result in src.read_dir()? {       // opening dir could fail
        let entry = entry_result?;              // reading dir could fail
        let dst_file = dst.join(entry.file_name());
        fs::rename(entry.path(), dst_file)?;    // renaming could fail
    }
    Ok(()) // phew!
}

? also works similarly with the Option type. In a function that returns Option, you can use ? to unwrap a value and return early in the case of None:

let weather = get_weather(hometown).ok()?;

Working with Multiple Error Types

Reading numbers from a text file:

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

/// Read integers from a text file.
/// The file should have one number on each line.
fn read_numbers(file: &mut dyn BufRead) -> Result<Vec<i64>, io::Error> {
    let mut numbers = vec![];
    for line_result in file.lines() {
        let line = line_result?;        // reading lines can fail
        numbers.push(line.parse()?);    // parsing integers can fail
    }
    Ok(numbers)
}
// error[E0277]: `?` couldn't convert the error to `std::io::Error`
// |
// |     fn read_numbers(file: &mut dyn BufRead) -> Result<Vec<i64>, io::Error> {
// |                                                --------------------------- expected `std::io::Error` because of this
// ...
// |             numbers.push(line.parse()?);
// |                                      ^ the trait `From<ParseIntError>` is not implemented for `std::io::Error`
  • Rust is complaining that the ? operator can’t convert a ParseIntError value to the type std::io::Error. Reading a line from a file and parsing an integer produce two different potential error types. Rust tries to cope with the ParseIntError by converting it to a io::Error, but there’s no such conversion

There are several ways of dealing with this. For example, the image crate defines its own error type, ImageError, and implements conversions from io::Error and several other error types to ImageError. If you’d like to go this route, try the thiserror crate, which is designed to help you define good error types with just a few lines of code.

A simpler approach is to use what’s built into Rust. All of the standard library error types can be converted to the type Box<dyn std::error::Error + Send + Sync + 'static>. dyn std::error::Error represents “any error,” and Send + Sync + 'static makes it safe to pass between threads. For convenience, you can define type aliases:

type GenericError = Box<dyn std::error::Error + Send + Sync + 'static>;
type GenericResult<T> = Result<T, GenericError>;
  • Change the return type of read_numbers() to GenericResult<Vec<i64>>. With this change, the function compiles. The ? operator automatically converts either type of error into a GenericError as needed.

  • The downside of the GenericError approach is that the return type no longer communicates precisely what kinds of errors the caller can expect. The caller must be ready for anything.

    • If you’re calling a function that returns a GenericResult and you want to handle one particular kind of error but let all others propagate out, use the generic method error.downcast_ref::<ErrorType>(). It borrows a reference to the error, if it happens to be the particular type of error you’re looking for:

      loop {
          match compile_project() {
              Ok(()) => return Ok(()),
              Err(err) => {
                  if let Some(mse) = err.downcast_ref::<MissingSemicolonError>() {
                      insert_semicolon_in_source_code(mse.file(), mse.line())?;
                      continue; // try again!
                  }
                  return Err(err);
              }
          }
      }
      
  • Also consider using the popular anyhow crate, which provides error and result types very much like GenericError and GenericResult, but with some nice additional features.

The ? operator does this automatic conversion using a standard method that you can use yourself. To convert any error to the GenericError type, call GenericError::from():

let io_error = io::Error::new( // make our own io::Error
    io::ErrorKind::Other, "timed out");
return Err(GenericError::from(io_error)); // manually convert to GenericError

Dealing with Errors That “Can’t Happen”

Suppose we’re writing code to parse a configuration file, and at one point we find that the next thing in the file is a string of digits. We want to convert this string of digits to an actual number. There’s a standard method that does this:

let num = digits.parse::<u64>();

The str.parse::<u64>() method doesn’t return a u64. It returns a Result. It can fail, because some strings aren’t numeric.

We happen to know that in this case, digits consists entirely of digits. If the code we’re writing already returns a GenericResult, we can tack on a ? and forget about it. Otherwise, we face the irritating prospect of having to write error-handling code for an error that can’t happen. The best choice then would be to use .unwrap(), a Result method that panics if the result is an Err, but simply returns the success value of an Ok:

let num = digits.parse::<u64>().unwrap();

"99999999999999999999".parse::<u64>() // overflow error
  • This is just like ? except that if we’re wrong about this error, if it can happen, then in that case we would panic. But if the input contains a long enough string of digits, the number will be too big to fit in a u64. Using .unwrap() in this particular case would therefore be a bug. Bogus input shouldn’t cause a panic.

  • Situations do come up where a Result value truly can’t be an error. For example, the Write trait defines a common set of methods (.write() and others) for text and binary output. All of those methods return io::Results, but if you happen to be writing to a Vec<u8>, they can’t fail. In such cases, it’s acceptable to use .unwrap() or .expect(message) to dispense with the Results.

  • These methods are also useful when an error would indicate a condition so severe or bizarre that panic is exactly how you want to handle it:

    fn print_file_age(filename: &Path, last_modified: SystemTime) {
        let age = last_modified.elapsed().expect("system clock drift");
        // ...
    }
    
    • The .elapsed() method can fail only if the system time is earlier than when the file was created. This can happen if the file was created recently, and the system clock was adjusted backward while our program was running.

Ignoring Errors

In the print_error() function, we had to handle the unlikely situation where printing the error triggers another error. This could happen, for example, if stderr is piped to another process, and that process is killed. The original error we were trying to report is probably more important to propagate, so we just want to ignore the troubles with stderr, but the Rust compiler warns about unused Result values:

fn print_error(mut err: &dyn Error) {
    writeln!(stderr(), "error: {}", err); // warning: unused result
    while let Some(source) = err.source() {
        let _ = writeln!(stderr(), "caused by: {}", source);
        err = source;
    }
}
  • The idiom let _ = ... is used to silence this warning:

    let _ = writeln!(stderr(), "error: {}", err); // ok, ignore result
    

Handling Errors in main()

In most places where a Result is produced, letting the error bubble up to the caller is the right behavior. But if you propagate an error long enough, eventually it reaches main(), and something has to be done with it. Normally, main() can’t use ? because its return type is not Result.

The simplest way to handle errors in main() is to use .expect():

fn main() {
    calculate_tides().expect("error"); // the buck stops here
}
  • The error message is lost in the noise. Also, RUST_BACKTRACE=1 is bad advice in this particular case.

Panicking in the main thread prints an error message and then exits with a nonzero exit code.

You can also change the type signature of main() to return a Result type, so you can use ?:

fn main() -> Result<(), TideCalcError> {
    let tides = calculate_tides()?;
    print_tides(tides);
    Ok(())
}
  • This works for any error type that can be printed with the {:?} formatter, which all standard error types, like std::io::Error, can be.

  • This technique is easy to use and gives a somewhat nicer error message, but it’s not ideal. If you have more complex error types or want to include more details in your message, it pays to print the error message yourself:

    fn main() {
        if let Err(err) = calculate_tides() {
            print_error(&err);
            std::process::exit(1);
        }
    }
    

Declaring a Custom Error Type

use std::fmt;

// json/src/error.rs
#[derive(Debug, Clone)]
pub struct JsonError {
    pub message: String,
    pub line: usize,
    pub column: usize,
}

// Errors should be printable.
impl fmt::Display for JsonError {
    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
        write!(f, "{} ({}:{})", self.message, self.line, self.column)
    }
}

// Errors should implement the std::error::Error trait,
// but the default definitions for the Error methods are fine.
impl std::error::Error for JsonError { }

return Err(JsonError {
    message: "expected ']' at end of array".to_string(),
    line: current_line,
    column: current_column
});

thiserror crate:

use thiserror::Error;

#[derive(Error, Debug)]
#[error("{message:} ({line:}, {column})")]
pub struct JsonError {
    message: String,
    line: usize,
    column: usize,
}
  • The #[derive(Error)] directive tells thiserror to generate the code shown earlier.

Why Results?

The key points of the design of choosing Results over exceptions:

  • Rust requires the programmer to make some sort of decision, and record it in the code, at every point where an error could occur. This is good because otherwise it’s easy to get error handling wrong through neglect.
  • The most common decision is to allow errors to propagate, and that’s written with a single character, ?. Thus, error plumbing does not clutter up your code the way it does in C and Go. Yet it’s still visible: you can look at a chunk of code and see at a glance all places where errors are propagated.
  • Since the possibility of errors is part of every function’s return type, it’s clear which functions can fail and which can’t. If you change a function to be fallible, you’re changing its return type, so the compiler will make you update that function’s downstream users.
  • Rust checks that Result values are used, so you can’t accidentally let an error pass silently (a common mistake in C).
  • Since Result is a data type like any other, it’s easy to store success and error results in the same collection. This makes it easy to model partial success. For example, if you’re writing a program that loads millions of records from a text file and you need a way to cope with the likely outcome that most will succeed, but some will fail, you can represent that situation in memory using a vector of Results.

References