Controlling panics with std::panic
There is a std::panic
module, which includes methods for halting theunwinding process started by a panic:
#![allow(unused_variables)]
fn main() {
use std::panic;
let result = panic::catch_unwind(|| {
println!("hello!");
});
assert!(result.is_ok());
let result = panic::catch_unwind(|| {
panic!("oh no!");
});
assert!(result.is_err());
}
In general, Rust distinguishes between two ways that an operation can fail:
- Due to an expected problem, like a file not being found.
- Due to an unexpected problem, like an index being out of bounds for an array.
Expected problems usually arise from conditions that are outside of yourcontrol; robust code should be prepared for anything its environment might throwat it. In Rust, expected problems are handled via the Result
type,which allows a function to return information about the problem to its caller,which can then handle the error in a fine-grained way.
Unexpected problems are bugs: they arise due to a contract or assertion beingviolated. Since they are unexpected, it doesn't make sense to handle them in afine-grained way. Instead, Rust employs a "fail fast" approach by panicking,which by default unwinds the stack (running destructors but no other code) ofthe thread which discovered the error. Other threads continue running, but willdiscover the panic any time they try to communicate with the panicked thread(whether through channels or shared memory). Panics thus abort execution up tosome "isolation boundary", with code on the other side of the boundary stillable to run, and perhaps to "recover" from the panic in some very coarse-grainedway. A server, for example, does not necessarily need to go down just because ofan assertion failure in one of its threads.
It's also worth noting that programs may choose to abort instead of unwind,and so catching panics may not work. If your code relies on catch_unwind
, youshould add this to your Cargo.toml:
[profile.dev]
panic = "unwind"
[profile.release]
panic = "unwind"
If any of your users choose to abort, they'll get a compile-time failure.
The catchunwind
API offers a way to introduce new isolation boundaries_within a thread. There are a couple of key motivating examples:
- Embedding Rust in other languages
- Abstractions that manage threads
- Test frameworks, because tests may panic and you don't want that to kill the test runner
For the first case, unwinding across a language boundary is undefined behavior,and often leads to segfaults in practice. Allowing panics to be caught meansthat you can safely expose Rust code via a C API, and translate unwinding intoan error on the C side.
For the second case, consider a threadpool library. If a thread in the poolpanics, you generally don't want to kill the thread itself, but rather catch thepanic and communicate it to the client of the pool. The catch_unwind
API ispaired with resume_unwind
, which can then be used to restart the panickingprocess on the client of the pool, where it belongs.
In both cases, you're introducing a new isolation boundary within a thread, andthen translating the panic into some other form of error elsewhere.