#![doc = include_str!("../README.md")]
#![expect(
clippy::needless_doctest_main,
reason = "README.md contains example usage with a `fn main()` that also runs as a doctest"
)]
mod panic_data;
mod panic_hook;
mod thread_local_catch_stack;
use std::panic::UnwindSafe;
pub use panic_data::{PanicData, PanicLocation};
use crate::thread_local_catch_stack::{
CaptureBacktrace, CatchStackFrame, THREAD_LOCAL_CATCH_STACK,
};
pub type Result<T> = std::result::Result<T, PanicData>;
pub fn catch<F: FnOnce() -> R + UnwindSafe, R>(f: F) -> Result<R> {
catch_inner(f, CaptureBacktrace::Default)
}
pub fn catch_force_backtrace<F: FnOnce() -> R + UnwindSafe, R>(f: F) -> Result<R> {
catch_inner(f, CaptureBacktrace::Always)
}
pub fn catch_never_backtrace<F: FnOnce() -> R + UnwindSafe, R>(f: F) -> Result<R> {
catch_inner(f, CaptureBacktrace::Never)
}
fn catch_inner<F: FnOnce() -> R + UnwindSafe, R>(
f: F,
capture_backtrace: CaptureBacktrace,
) -> Result<R> {
if let Err(()) = panic_hook::install_if_not_installed() {
panic!("the first call to `chillpill::catch` must not be made from a panicking thread");
}
THREAD_LOCAL_CATCH_STACK.with_borrow_mut(|stack| {
stack.push(CatchStackFrame::new(capture_backtrace));
});
let catch_unwind_result = std::panic::catch_unwind(f);
let frame = THREAD_LOCAL_CATCH_STACK
.with_borrow_mut(Vec::pop)
.expect("catch stack should not be empty, since we just pushed a frame - this is a bug in chillpill");
catch_unwind_result.map_err(|payload| {
let location = frame.location;
let backtrace = frame.backtrace;
PanicData {
payload,
location,
backtrace,
}
})
}
#[cfg(test)]
mod tests {
use std::panic::AssertUnwindSafe;
use super::*;
#[test]
fn basic_code_no_panic() {
assert_eq!(catch(|| 2 + 2).unwrap(), 4);
assert_eq!(catch(|| String::from("it works!")).unwrap(), "it works!");
}
#[test]
fn catches_basic_panic() {
let result = catch(|| {
panic!("uh oh spaghettio");
})
.unwrap_err();
assert_eq!(result.payload_as_string().unwrap(), "uh oh spaghettio");
}
macro_rules! panic_and_get_location {
($location:ident $(, $($arg:tt)*)*$(,)?) => {
$location = ::core::option::Option::Some($crate::PanicLocation {
file: String::from(file!()),
line: line!(),
col: column!(),
});
panic!($($($arg),*)*)
};
}
#[test]
fn captures_location() {
let mut location = None;
let result = catch(AssertUnwindSafe(|| {
panic_and_get_location!(location, "I'm freakin' out!!!");
}))
.unwrap_err();
assert_eq!(result.payload_as_string().unwrap(), "I'm freakin' out!!!");
assert_eq!(result.location, location);
}
#[test]
fn non_string_payload() {
let result = catch(|| {
let payload: Vec<i32> = vec![1, 2, 3];
std::panic::panic_any(payload);
})
.unwrap_err();
assert_eq!(*result.payload.downcast::<Vec<i32>>().unwrap(), &[1, 2, 3]);
}
#[test]
fn nested_catches() {
let mut location1 = None;
let result1 = catch(AssertUnwindSafe(|| {
let mut location2 = None;
let result2 = catch(AssertUnwindSafe(|| {
let mut location3 = None;
let result3 = catch(AssertUnwindSafe(|| {
panic_and_get_location!(location3, "panic depth 3");
}))
.unwrap_err();
assert_eq!(result3.payload_as_string().unwrap(), "panic depth 3");
assert_eq!(result3.location, location3);
panic_and_get_location!(location2, "panic depth 2");
}))
.unwrap_err();
assert_eq!(result2.payload_as_string().unwrap(), "panic depth 2");
assert_eq!(result2.location, location2);
panic_and_get_location!(location1, "panic depth 1");
}))
.unwrap_err();
assert_eq!(result1.payload_as_string().unwrap(), "panic depth 1");
assert_eq!(result1.location, location1);
}
#[test]
fn catch_unwind_catches_only_panic() {
catch(AssertUnwindSafe(|| {
let _ = std::panic::catch_unwind(|| {
panic!("this panic should not make it to the outer catch");
});
}))
.unwrap();
let mut location = None;
let result = catch(AssertUnwindSafe(|| {
panic_and_get_location!(location, "unrelated later panic");
}))
.unwrap_err();
assert_eq!(result.payload_as_string().unwrap(), "unrelated later panic");
assert_eq!(result.location, location);
}
#[test]
fn catch_unwind_and_uncaught_panic() {
let mut location = None;
let result = catch(AssertUnwindSafe(|| {
let _ = std::panic::catch_unwind(|| {
panic!("this panic is irrelevant");
});
panic_and_get_location!(location, "actual panic");
}))
.unwrap_err();
assert_eq!(result.payload_as_string().unwrap(), "actual panic");
assert_eq!(result.location, location);
let mut location = None;
let result = catch(AssertUnwindSafe(|| {
panic_and_get_location!(location, "unrelated later panic");
}))
.unwrap_err();
assert_eq!(result.payload_as_string().unwrap(), "unrelated later panic");
assert_eq!(result.location, location);
}
}