Rust Error Handling With panic!

Rust Error Handling With panic!

In Rust, the panic! macro is used to handle unrecoverable errors.

There are two types of errors in Rust: recoverable errors, and unrecoverable errors. Error handling is different depending on what type of error it is.

Unrecoverable errors are generally caused by a bug that could lead to a serious issue, like a security vulnerability. When this occurs, we want the program to terminate immediately. This is where the panic! macro comes in.

What is Panic?

Panic is Rust’s response to an unrecoverable error. A panic can also be induced by explicitly calling the panic! macro. In either case, a panic causes the program to immediately terminate.

When a program panics, by default it will clean up allocated memory by unwinding the stack, but this feature can be disabled.

What Causes a Panic?

There are two ways to cause a panic in Rust:

  1. By doing something that causes the code to panic (for example, by using unsafe code).
  2. By explicitly calling the panic! macro.

What Happens During a Panic?

Whenever a panic occurs, a few things happen:

  1. A failure message gets printed to stderr.
  2. Either:
    • The stack gets unwound and memory cleaned before exiting, or:
    • The program aborts without unwinding the stack.

Unwinding vs. Aborting

By default, Rust will unwind the stack when a panic occurs. This means that the compiler walks its’ way up the stack, cleaning up the memory allocated to each function as it goes.

However, this takes a lot of work and it may sometimes be a better idea for the program to terminate without cleaning up all of the allocated memory. To do this, we can choose to have the program abort, rather than unwind, when a panic occurs.

To have a program abort instead of unwind, add the code panic = 'abort' to the corresponding [profile] sections in the cargo.toml file.

Calling the panic! Macro

We can call the panic! macro explicitly, at any point in our code. This can be useful, especially when writing test code:

fn main() {
    panic!("time to panic!");
}

Standard Error:

Compiling panic v0.1.0
    Finished dev [unoptimized + debuginfo] target(s) in 0.28s
     Running `target/debug/panic`
thread 'main' panicked at 'time to panic!', src/main.rs:2:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

The second to last lines of the error contains the text “time to panic!”, as well as the exact location of the panic: src/main.rs:2:5 tells us that it occurred on the second line, five characters in.

The last line gives us details for how to perform a backtrace that can give us more information about why the code panicked. In this case, we know why: we explicitly called the panic! macro. However in cases where the code panics on its’ own, it can be helpful to backtrace in order to identify more information about the issue.

Code Panic Due to a Bug

The panic! macro can be useful, in particular when testing code. But our code can also panic when we introduce a bug or do something unsafe. Let’s look at an example.

In the following case, we first create an array consisting of three integers. Then we try to print the element with an index of ‘4’, which is beyond the end of the array.

fn main() {
    let my_array = [1,2,3];
    println!("{}", my_array[4]);
}

Standard Error:

Compiling playground v0.0.1 (/playground)
error: this operation will panic at runtime
 --> src/main.rs:3:20
  |
3 |     println!("{}", my_array[4]);
  |                    ^^^^^^^^^^^ index out of bounds: the length is 3 but the index is 4
  |
  = note: `#[deny(unconditional_panic)]` on by default

error: could not compile `playground` due to previous error

We can see the location of the error: src/main.rs:3:20. This indicates that the panic occurs on the third line, 20 characters in. This is as we expected; the error also tells us that the index is out of bounds because the length of the array is 3 but the index called is 4. We also get an additional detail that tells us about the default lint ‘unconditional_panic‘.

Linting is a type of automated check that checks for errors, and in this case it does a good job in preventing us from compiling our code.

But what if linting doesn’t catch an error? We still rely on panic to prevent buggy code from executing.

Take the following example, which is very similar to the previous one. In this case, we create a vector (instead of an array) that consists of three elements of the integer type. We again try to access the element with an index of 4, which does not exist:

fn main() {
    let my_vector = vec![1, 2, 3];
    println!("{}", my_vector[4]);
}

Standard Error:

Compiling playground v0.0.1 (/playground)
    Finished dev [unoptimized + debuginfo] target(s) in 0.59s
     Running `target/debug/playground`
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 4', src/main.rs:3:20
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

In this case, the error was not detected by the unconditional_panic lint. Instead, the code panicked when it reached the error on line 3.