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),
}