14 January 2023
Rust has two methods for error handling: one for unrecoverable errors and one for recoverable errors, which I’ll go over in this and the next article.
An unrecoverable error is called a panic. It typically indicates an error in your code. A good example is what happens when we try to access a vector with an out-of-bounds index:
fn out_of_bounds() {
let v = vec![1, 2, 3];
println!("{}", v[3]);
}
fn main() {
println!("before calling out_of_bounds");
out_of_bounds();
println!("after calling out_of_bounds");
}
Running this with the default settings produces the following output:
before calling out_of_bounds
thread 'main' panicked at 'index out of bounds: the len is 3 but the
index is 3', src/main.rs:3:20
note: run with `RUST_BACKTRACE=1` environment variable to display a
backtrace
When the running program encounters the invalid index, it prints an error message explaining why it’s panicking, then “unwinds the stack” by jumping out of the current function, jumping out of the calling function, and so on until it’s jumped out of the main function and the program ends. It’ll still clean up the data in each function, deallocating memory and calling destructors. We can see this in action with the Data type from my previous article on the stack and the heap:
fn out_of_bounds() {
let d1 = Data(1);
let v = vec![1, 2, 3];
println!("{}", v[3]);
}
fn main() {
let d2 = Data(2);
println!("before calling out_of_bounds");
out_of_bounds();
println!("after calling out_of_bounds");
}
Here’s the output:
before calling out_of_bounds
thread 'main' panicked at 'index out of bounds: the len is 3 but the
index is 3', src/main.rs:13:20
note: run with `RUST_BACKTRACE=1` environment variable to display a
backtrace
dropping d1
dropping d2
You can see the runtime going through the calling fuctions and cleaning up local variables after
printing the message about the panic. If you set the RUST_BACKTRACE
variable as the
“note” suggests, then instead of the note you’ll see a trace of functions calls, each with source
file, line, and column. I’m not showing it here because it’s quite verbose, even for our simple
example program.
You can use the panic
macro to explicitly cause a panic:
fn explicit_panic() {
panic!("oops!");
}
fn main() {
println!("before calling explicit_panic");
explicit_panic();
println!("after calling explicit_panic");
}
The result is much the same as with the out-of-bounds error:
before calling explicit_panic
thread 'main' panicked at 'oops!', src/main.rs:2:5
note: run with `RUST_BACKTRACE=1` environment variable to display a
backtrace
An explicit panic can be useful when your program hits a case that “should never happen”, although the more specific unreachable macro is often a better choice. There’s also a neat todo macro that you can use during development for things you haven’t gotten around to yet.
If you don’t want the stack unwinding logic in your program, you can actually turn it off with the
panic setting. If you
add the following to your Cargo.toml
:
[profile.release]
panic = 'abort'
and use cargo --release
to build or run the code, then a panic will still print the
same error message as before, but then exit immediately. Running the sample code from before that’s
using the Data variables like this, we get the following output:
before calling out_of_bounds
thread 'main' panicked at 'index out of bounds:
the len is 3 but the index is 3', src/main.rs:13:20
note: run with `RUST_BACKTRACE=1` environment variable to display a
backtrace
Aborted (core dumped)
Notice there’s no mention of the drop
method anymore. Leaving out the logic for stack
unwinding make your binaries smaller -- for my sample code the difference was only about 5 KiB,
but I assume it’ll be more for larger programs.
I’ve talked about “unrecoverable” errors above, but you can actually recover from a panic using the
catch_unwind standard library
function. This is rather limited, though: the error message about panicking will still be printed to
stderr, and if panic = 'abort'
is set, it won’t do anything. From the documentation it
sounds like catch_unwind is meant for some specific scenarios such as C code calling into Rust, where
the unwind mechanism doesn’t work.