Error Handling

Panic is a kind of error that should never happen.

Results typically represent problems caused by things outside the program, like erroneous input, a network outage, or a permissions problem.

Panic

Rust can either unwind the stack when a panic happens or abort the process. Unwinding is the default.

Unwinding

If you set the RUST_BACKTRACE environment variable, Rust will dump the stack on panic. Panic is per thread. One thread can be panicking while other threads are going on about their normal business.

There is also a way to catch stack unwinding, allowing the thread to survive and continue running. The standard library function std::panic::catch_unwind() does this.

Aborting

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

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

  • Also, Rust’s panic behavior is customizable. If you compile with -C panic=abort, the first panic in your program immediately aborts the process.

Result

functions that can fail have a return type that says so:

fn get_weather(location: LatLng) -> Result<WeatherReport, io::Error>

Catching Errors

The most thorough way of dealing with a Result :

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

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.

Methods for working with references in a Result:

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

Result Type Aliases

Sometimes you’ll see Rust documentation that seems to omit the error type of a Result:

fn remove_file(path: &Path) -> Result<()>

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.

Printing Errors

The standard library defines several error types with boring 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.

err.to_string(): Returns an error message as a String.

err.source(): Returns an Option of the underlying error, if any, that caused err.

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.

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

fn print_error(err: &dyn Error) {
  let _ = writeln!(stderr(), "error: {}", err);
  while let Some(source) = err.source() {
    let _ = writeln!(stderr(), "caused by: {}", source);
    err = source;
  }
}

Propagating Errors

If an error occurs, we usually want to let our caller deal with it. We want errors to propagate up the call stack.

let weather = get_weather(home_town)?;
  • 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.

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

[!important]

Result and Option is defiend in Rust Core Library and can be used in nostd environment.

Working with Multiple Error Types

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.

type GenericError = Box<dyn std::error::Error + Send + Sync + 'static>;
type GenericResult<T> = Result<T, GenericError>;

Incidentally, the ? operator does the automatic conversion from random error types to GenericError 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

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;
      }
      return Err(err);
    }
  }
}

Dealing with Errors That “Can’t Happen”

If we are sure that an error cannot happen, then we can use unwrap() on the Result type:

// We are sure that `digits` can be parsed as a number.
let num = digits.parse::<u64>().unwrap();

However, this could cause panics since 999999999999 will cause an overflow error even if can be parsed as an number.

Ignoring Errors

writeln!(stderr(), "error: {}", err); can return a Result, which could indicate an Err. We can ignore this by:

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

Declaring a Custom Error Type

// json/src/error.rs
use std::fmt;

#[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
});

You can make error handling much easier by the thiserror crate:

use thiserror::Error;

#[derive(Error, Debug)]
#[error("{message:} ({line:}, {column})")]
pub struct JsonError {
    message: String,
    line: usize,
    column: usize,
}

Last updated