Error Handling

There are two types of error-handling in Rust:

  1. panic
  2. Results

Ordinary errors are handled using Results.

Panic is the bad kind, it's for errors that should never happen.

Panic

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

These are some things that can cause a panic:

  • Out-of-bounds array access
  • Integer division by zero
  • Calling .unwrap() on an Option that happens to be None
  • Assertion failure

Behavior: When a program panics, you can choose one of two ways that it'll be handled:

  1. Unwind the stack (this is the default)
  2. Abort the process

Unwinding

Process of a panic-triggered unwinding

  1. An error message is printed to the terminal.
  2. The stack is unwound.
  3. Any temporary values, local variables, or arguments that the current function was using are dropped, in the reverse of the order they were created. This perpetuates upwards through the unwound stack.
  4. 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, nor undefined behavior. A panic's behavior is well-defined and safe.

Behavior: Panics occur per thread. One thread can be panicking while other threads are going on about their normal business.

It's possible to catch stack unwinding, which would allow the thread to survive and continue running using the standard library function std::panic::catch_unwind().

Aborting

Stack unwinding is the default panic behavior, but

Behavior: 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. The process will be aborted.
  2. If you compile with a -C panic=abort flat, the first panic in the program immediately aborts the process. (this can be used to reduce compiled code size)

Result

Rust doesn't have exceptions.

Catching Errors

The most thorough way of dealing with errors via Result is using a match expression:


#![allow(unused)]
fn main() {
match get_weather(hometown) {
    Ok(result) => {
        display_weather(hometown, &report);
    }
    Error(err) => {
        println!("error querying the weather: {}", err);
        schedule_weather_retry();
    }
}
}

matches can be a bit verbose. But, Result comes with a ton of useful methods for more concise handling.

Add notes about the `Result` methods starting on page 148.

Result Type Aliases

Sometimes you'll see Rust documentation that seems to omit the error type of a Result. In such cases, a Result type alias (a type alias is a shorthand for type names) is being used:

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

Printing Errors

All error types implement a common trait: std::error::Error.

Warning: Printing an error value does not also print out its cause. If you want to print all available information for an error, use the print_error function defined below.


#![allow(unused)]
fn main() {
use std::error::Error;
use std::io::{Write, stderr};
/// Dump an error message to `stderr`
/// If another error occurs in the process, ignore it
fn print_error(mut err: &Error) {
    let _ = writeln!(stderr(), "error: {}", err);
    while let Some(cause) = err.cause() {
        let _ = writeln!(stderr(), "caused by: {}", cause);
        err = cause;
    }
}
}

Crate: The standard library's error types do not include a stack trace, but the error-chain crate makes it easy to define your own custom error type that supports grabbing a stack trace when it's created. It uses the backtrace crate to capture the stack.

Propagating Errors

Operator: You can add a ? to any expression that produces a Result. The behavior of ? depends on the state (Ok or Error of the Result):

  • If Ok, it unwraps the Result to get the success value inside
  • If Error, it immediately returns from the enclosing function, passing the error result up the call chain (see the rule below)

Rule: The ? can only be used in functions whose return value is of type Result.

Working with Multiple Error Types

Some functions have the potential to return Errors of a many different type (depending on the operation that triggered the error).

There are several approaches to dealing with multiple error types:

  1. Conversion: Define a custom error type (say, CustomError) and implement conversions from io::Error to the custom error type.
  2. Box 'em up: The simpler approach is to use pointers. All error types can be converted to the type Box<std::error::Error>, which represents "any error", so we can define a set of generic type aliases all possible errors. This is the most idiomatic approach..

For generalizing all errors and results, define these type aliases:


#![allow(unused)]
fn main() {
type GenError = Box<std::error::Error>;
type GenResult<T> = Result<T, GenError>;
}

Tip: To convert any error to the GenError type, call GenError::from().

The downside of the GenError approach is that the return type no longer communicates precisely what kinds of errors the caller can expect.

Tip: If you want to handle on particular kind of error, but let all other propagate out, use the generic method error.downcast_ref::<ErrorType>(). This is called error downcasting.

Dealing with Errors That "Can't Happen"

Operator: Instead of the ? operator, which requires implementing error-handling, we can use the .unwrap() method of a Result to get the Ok value.

Warning: The difference between ? and .unwrap() is that if .unwrap() is used on Result that's in its Error state, the process will panic. In other words, only use .unwrap() when you're damn sure Result is Ok.

Ignoring Errors

Idiom: If we really don't care about the contents of a Result, we can use the following idiomatic statement to silence warnings about unused results:

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

Handling Errors in main()

If you propagate an error long enough, eventually it'll reach the root main() function, at which point, it can no longer be ignored.

Info: The ? operator cannot be used in main because main's return type is not a Result. Instead, use .expect().

Behavior: Panicking in the main thread print an error message then exits with a nonzero exit code.