Dillan Hildebrand

Senior Software Engineer

Error Handling in Rust

In Rust, there are two different types of errors to handle - panics and Results.

Panics

A panic will occur when there’s an issue with the code of a program. For example; trying to divide by zero, out-of-bound access on arrays, calling .expect() on a Result that happens to be Err, assertion failure, etc will all result in a panic.

From time to time, you may need to manually trigger a panic in code during runtime. You can manually trigger a panic in Rust using the panic!() macro.

In Rust, there are two different behaviors that can happen when a panic occurs. It can abort and exit the process right away with a non-zero exit code.. Or it can unwind the stack and provide a backtrace. Unwinding is the default behavior.

Unwinding

Let’s take a look at the unwinding behavior by manually invoking a panic with the panic macro:

fn main() {
    panic!("Something went terribly wrong!")
}

When building and running the code, we get the following output:

$ cargo run
    Finished dev profile [unoptimized + debuginfo] target(s) in 0.02s
     Running target/debug/rust-error-handling
thread 'main' panicked at src/main.rs:2:5:
Somethi ng went terribly wrong
note: run with RUST_BACKTRACE=1 environment variable to display a backtrace

Rust tells us where the panic occurred in our debug build, along with the panic message. It also informs us that if we want to display the stack backtrace, we need to export RUST_BACKTRACE=1. Let’s see what that looks like:

$ RUST_BACKTRACE=1 cargo run
    Finished dev profile [unoptimized + debuginfo] target(s) in 0.00s
     Running target/debug/rust-error-handling
thread 'main' panicked at src/main.rs:2:5:
Something went terribly wrong
stack backtrace:
   0: rust_begin_unwind
             at /rustc/9b00956e56009bab2aa15d7bff10916599e3d6d6/library/std/src/panicking.rs:645:5
   1: core::panicking::panic_fmt
             at /rustc/9b00956e56009bab2aa15d7bff10916599e3d6d6/library/core/src/panicking.rs:72:14
   2: rust_error_handling::main
             at ./src/main.rs:2:5
   3: core::ops::function::FnOnce::call_once
             at /rustc/9b00956e56009bab2aa15d7bff10916599e3d6d6/library/core/src/ops/function.rs:250:5
note: Some details are omitted, run with RUST_BACKTRACE=full for a verbose backtrace.

Rust has varying levels of verbosity when it comes to stack backtrace output. We can see Rust is still holding some details back from us which it tells us can be output by exporting RUST_BACKTRACE=full

$ RUST_BACKTRACE=full cargo run
    Finished dev profile [unoptimized + debuginfo] target(s) in 0.00s
     Running target/debug/rust-error-handling
thread 'main' panicked at src/main.rs:2:5:
Something went terribly wrong
stack backtrace:
   0:        0x102e99210 - std::backtrace_rs::backtrace::libunwind::trace::h6de1cbf3f672a4f8
                               at /rustc/9b00956e56009bab2aa15d7bff10916599e3d6d6/library/std/src/../../backtrace/src/backtrace/libunwind.rs:105:5
   1:        0x102e99210 - std::backtrace_rs::backtrace::trace_unsynchronized::hd0de2d5ef13b6f4d
                               at /rustc/9b00956e56009bab2aa15d7bff10916599e3d6d6/library/std/src/../../backtrace/src/backtrace/mod.rs:66:5
   2:        0x102e99210 - std::sys_common::backtrace::_print_fmt::h2a33510d9b3bb866
                               at /rustc/9b00956e56009bab2aa15d7bff10916599e3d6d6/library/std/src/sys_common/backtrace.rs:68:5
   3:        0x102e99210 - <std::sys_common::backtrace::_print::DisplayBacktrace as core::fmt::Display>::fmt::h01b2beffade888b2
                               at /rustc/9b00956e56009bab2aa15d7bff10916599e3d6d6/library/std/src/sys_common/backtrace.rs:44:22
   4:        0x102eae668 - core::fmt::rt::Argument::fmt::h5ddc0f22b2928899
                               at /rustc/9b00956e56009bab2aa15d7bff10916599e3d6d6/library/core/src/fmt/rt.rs:142:9
   5:        0x102eae668 - core::fmt::write::hbadb443a71b75f23
                               at /rustc/9b00956e56009bab2aa15d7bff10916599e3d6d6/library/core/src/fmt/mod.rs:1153:17
   6:        0x102e97674 - std::io::Write::write_fmt::hc09d7755e3ead5f0

    <snipped>

  26:        0x102e95e00 - std::panicking::try::h8aa812e3e1310d12
                               at /rustc/9b00956e56009bab2aa15d7bff10916599e3d6d6/library/std/src/panicking.rs:516:19
  27:        0x102e95e00 - std::panic::catch_unwind::h38c4879f2623185e
                               at /rustc/9b00956e56009bab2aa15d7bff10916599e3d6d6/library/std/src/panic.rs:146:14
  28:        0x102e95e00 - std::rt::lang_start_internal::h39923ab4c3913741
                               at /rustc/9b00956e56009bab2aa15d7bff10916599e3d6d6/library/std/src/rt.rs:148:20
  29:        0x102e81a14 - std::rt::lang_start::h6f29b8c23cada729
                               at /rustc/9b00956e56009bab2aa15d7bff10916599e3d6d6/library/std/src/rt.rs:165:17
  30:        0x102e818f0 - _main

When rust unwinds the stack, it starts where the error occurred. All temporary values, local variables, and arguments the current function was using, are dropped (aka freed) in the reverse order they were created. Once the current function call is all cleaned up, rust moves (or unwinds) its way up to the calling function and does the same cleanup, all the way up the stack. Once the stack is completely unwound, the thread which the error occurred in, will exit. If the thread is the main thread, the whole process exits.

Panics are per thread. One thread can panic while others continue running about their normal business.

Aborting

Stack unwinding is the default panic behavior in Rust. To switch from unwinding to aborting pass -C panic=abort when compiling your source with rustc. If you’re building with Cargo, add panic = "abort" under the [profile.release] section in your Cargo.toml file for release builds. If you want the abort panic behavior in other builds as well (say dev), simply add panic = "abort" to the desired profile [profile.dev].

$ RUST_BACKTRACE=1 cargo run
    Finished dev profile [unoptimized + debuginfo] target(s) in 0.00s
     Running target/debug/rust-error-handling
thread 'main' panicked at src/main.rs:2:5:
Something went terribly wrong
stack backtrace:
   0: rust_begin_unwind
             at /rustc/9b00956e56009bab2aa15d7bff10916599e3d6d6/library/std/src/panicking.rs:645:5
   1: core::panicking::panic_fmt
             at /rustc/9b00956e56009bab2aa15d7bff10916599e3d6d6/library/core/src/panicking.rs:72:14
   2: rust_error_handling::main
             at ./src/main.rs:2:5
note: Some details are omitted, run with RUST_BACKTRACE=full for a verbose backtrace.
[1]    76360 abort      RUST_BACKTRACE=1 cargo run

It should be pointed out that an abort can happen even when the panic behavior is set to unwinding. If a second panic is triggered while Rust is unwinding and cleaning up the stack, it will stop unwinding and abort the entire process.

Result

Rust doesn’t have exceptions. Instead, functions that can fail have a return type that indicate so. Result<T, E> is the type. T (retval type) is the return value. E is error. Ok(T) indicates success and is the return value. Err(E) indicates and contains the error when one occurs.

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

Here’s a basic example:

use std::env;

fn main() -> Result<(), String> {
    let args: Vec<String> = env::args().collect();
    let month_num: u8 = args[1].parse().expect(&format!("Failed parsing month num argument!"));
    let month = match month_num_to_name(month_num) {
        Ok(v) => v,
        Err(e) => return Err(e)
    };
    println!("{}", month);
    Ok(())
}

fn month_num_to_name(month_num: u8) -> Result<&'static str, String> {
    match month_num {
        1 => Ok("January"),
        2 => Ok("Febuary"),
        3 => Ok("March"),
        4 => Ok("April"),
        5 => Ok("May"),
        6 => Ok("June"),
        7 => Ok("July"),
        8 => Ok("Auguest"),
        9 => Ok("September"),
        10 => Ok("October"),
        11 => Ok("November"),
        12 => Ok("December"),
        _ => Err(format!("Invalid month '{month_num}' - must be 1-12."))
    }
}

The return type of month_num_to_name is Result<&'static str, String> which means it can either return Result.Ok(&'static str) or Result.Err(String) types.

Handling results

match

The most thorough way of handling results is with match:

let month = match month_num_to_name(month_num) {
    Ok(v) => v,
    Err(e) => return Err(e)
};
unwrap

Unwrap is a utility method that Result<T, E> implements. It takes the value from an Ok result. If the result is an Err, unwrap will panic.

let month = month_num_to_name(month_num).unwrap()

is equivelant to:

let month = match month_num_to_name(month_num) {
    Ok(v) => v,
    Err(e) => panic!("{}", e)
};
expect

Expect does the same thing as unwrap but it lets us pass a custom panic message.

let month = month_num_to_name(month_num).expect("Couldn't convert month num to name")

Result Type Aliases

@TODO

Printing errors

@TODO

Propagating errors

? operator

The ? operator can be used to propagate errors. It will return the result if it’s of type Err. Otherwise, it unwraps the value in the Ok result. Basically, it’s handling the match and unwrap for us.

Multiple error types

@TODO

Option

pub enum Option<T> {
    None,
    Some(T),
}
Tags: Rust