1014-recover-from-panic — Recover from Panic
Tutorial
The Problem
Rust's panics are designed for unrecoverable programming errors, but in server environments you sometimes need to isolate untrusted or user-supplied code: a plugin that panics should not crash the entire process. The std::panic::catch_unwind function provides a safety net — it catches a panic before it unwinds past the boundary and converts it to a Result.
This is the mechanism that web frameworks, test runners (like the built-in #[test] harness), and FFI boundaries use to prevent a single bad computation from bringing down the whole system.
🎯 Learning Outcomes
std::panic::catch_unwind to convert a panic into a ResultUnwindSafe and why mutable references require AssertUnwindSafeBox<dyn Any> using downcast_refcatch_unwind does not work with panic = "abort" buildsCode Example
#![allow(clippy::all)]
// 1014: Recover from Panic
// std::panic::catch_unwind for recovering from panics
use std::panic;
// Approach 1: catch_unwind — converts panic to Result
fn safe_divide(a: i64, b: i64) -> Result<i64, String> {
let result = panic::catch_unwind(|| {
if b == 0 {
panic!("division by zero");
}
a / b
});
result.map_err(|e| {
if let Some(s) = e.downcast_ref::<&str>() {
s.to_string()
} else if let Some(s) = e.downcast_ref::<String>() {
s.clone()
} else {
"unknown panic".into()
}
})
}
// Approach 2: catch_unwind with AssertUnwindSafe
fn catch_with_state(data: &mut Vec<i64>) -> Result<i64, String> {
// AssertUnwindSafe is needed for mutable references
let result = panic::catch_unwind(panic::AssertUnwindSafe(|| {
data.push(42);
if data.len() > 5 {
panic!("too many elements");
}
data.iter().sum()
}));
result.map_err(|e| {
e.downcast_ref::<&str>()
.map(|s| s.to_string())
.unwrap_or_else(|| "unknown".into())
})
}
// Approach 3: set_hook for custom panic handling
fn with_quiet_panic<F, R>(f: F) -> Result<R, String>
where
F: FnOnce() -> R + panic::UnwindSafe,
{
// Suppress default panic output
let prev_hook = panic::take_hook();
panic::set_hook(Box::new(|_| {})); // silent
let result = panic::catch_unwind(f);
panic::set_hook(prev_hook); // restore
result.map_err(|e| {
e.downcast_ref::<&str>()
.map(|s| s.to_string())
.unwrap_or_else(|| "unknown panic".into())
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_catch_success() {
assert_eq!(safe_divide(10, 2), Ok(5));
}
#[test]
fn test_catch_panic() {
let result = safe_divide(10, 0);
assert!(result.is_err());
assert!(result.unwrap_err().contains("division by zero"));
}
#[test]
fn test_catch_with_state() {
let mut data = vec![1, 2, 3];
let result = catch_with_state(&mut data);
assert!(result.is_ok());
assert_eq!(data.len(), 4); // 42 was pushed
}
#[test]
fn test_catch_state_overflow() {
let mut data = vec![1, 2, 3, 4, 5];
let result = catch_with_state(&mut data);
assert!(result.is_err());
}
#[test]
fn test_quiet_panic() {
let result = with_quiet_panic(|| {
panic!("silent failure");
});
assert!(result.is_err());
}
#[test]
fn test_quiet_success() {
let result = with_quiet_panic(|| 42);
assert_eq!(result, Ok(42));
}
#[test]
fn test_catch_unwind_basics() {
// catch_unwind returns Result<T, Box<dyn Any>>
let ok = std::panic::catch_unwind(|| 42);
assert_eq!(ok.unwrap(), 42);
let err = std::panic::catch_unwind(|| -> i64 { panic!("boom") });
assert!(err.is_err());
}
}Key Differences
try/with is the primary error-handling mechanism for exceptions; Rust's catch_unwind is a niche escape hatch not meant for normal control flow.UnwindSafe to prevent unsound state; OCaml has no such restriction.'static value; OCaml exceptions are typed variant constructors.panic = "abort"**: When compiled with panic = "abort", Rust processes terminate immediately on panic and catch_unwind has no effect; OCaml always allows exception catching.OCaml Approach
OCaml uses try ... with for exception recovery, which is a first-class language feature:
let safe_divide a b =
try Ok (a / b)
with Division_by_zero -> Error "division by zero"
OCaml exceptions carry structured payloads and can be caught at any level. They are not special: try is syntactic sugar for a pattern match on the exception type. There is no UnwindSafe concept because OCaml's GC handles all memory.
Full Source
#![allow(clippy::all)]
// 1014: Recover from Panic
// std::panic::catch_unwind for recovering from panics
use std::panic;
// Approach 1: catch_unwind — converts panic to Result
fn safe_divide(a: i64, b: i64) -> Result<i64, String> {
let result = panic::catch_unwind(|| {
if b == 0 {
panic!("division by zero");
}
a / b
});
result.map_err(|e| {
if let Some(s) = e.downcast_ref::<&str>() {
s.to_string()
} else if let Some(s) = e.downcast_ref::<String>() {
s.clone()
} else {
"unknown panic".into()
}
})
}
// Approach 2: catch_unwind with AssertUnwindSafe
fn catch_with_state(data: &mut Vec<i64>) -> Result<i64, String> {
// AssertUnwindSafe is needed for mutable references
let result = panic::catch_unwind(panic::AssertUnwindSafe(|| {
data.push(42);
if data.len() > 5 {
panic!("too many elements");
}
data.iter().sum()
}));
result.map_err(|e| {
e.downcast_ref::<&str>()
.map(|s| s.to_string())
.unwrap_or_else(|| "unknown".into())
})
}
// Approach 3: set_hook for custom panic handling
fn with_quiet_panic<F, R>(f: F) -> Result<R, String>
where
F: FnOnce() -> R + panic::UnwindSafe,
{
// Suppress default panic output
let prev_hook = panic::take_hook();
panic::set_hook(Box::new(|_| {})); // silent
let result = panic::catch_unwind(f);
panic::set_hook(prev_hook); // restore
result.map_err(|e| {
e.downcast_ref::<&str>()
.map(|s| s.to_string())
.unwrap_or_else(|| "unknown panic".into())
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_catch_success() {
assert_eq!(safe_divide(10, 2), Ok(5));
}
#[test]
fn test_catch_panic() {
let result = safe_divide(10, 0);
assert!(result.is_err());
assert!(result.unwrap_err().contains("division by zero"));
}
#[test]
fn test_catch_with_state() {
let mut data = vec![1, 2, 3];
let result = catch_with_state(&mut data);
assert!(result.is_ok());
assert_eq!(data.len(), 4); // 42 was pushed
}
#[test]
fn test_catch_state_overflow() {
let mut data = vec![1, 2, 3, 4, 5];
let result = catch_with_state(&mut data);
assert!(result.is_err());
}
#[test]
fn test_quiet_panic() {
let result = with_quiet_panic(|| {
panic!("silent failure");
});
assert!(result.is_err());
}
#[test]
fn test_quiet_success() {
let result = with_quiet_panic(|| 42);
assert_eq!(result, Ok(42));
}
#[test]
fn test_catch_unwind_basics() {
// catch_unwind returns Result<T, Box<dyn Any>>
let ok = std::panic::catch_unwind(|| 42);
assert_eq!(ok.unwrap(), 42);
let err = std::panic::catch_unwind(|| -> i64 { panic!("boom") });
assert!(err.is_err());
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_catch_success() {
assert_eq!(safe_divide(10, 2), Ok(5));
}
#[test]
fn test_catch_panic() {
let result = safe_divide(10, 0);
assert!(result.is_err());
assert!(result.unwrap_err().contains("division by zero"));
}
#[test]
fn test_catch_with_state() {
let mut data = vec![1, 2, 3];
let result = catch_with_state(&mut data);
assert!(result.is_ok());
assert_eq!(data.len(), 4); // 42 was pushed
}
#[test]
fn test_catch_state_overflow() {
let mut data = vec![1, 2, 3, 4, 5];
let result = catch_with_state(&mut data);
assert!(result.is_err());
}
#[test]
fn test_quiet_panic() {
let result = with_quiet_panic(|| {
panic!("silent failure");
});
assert!(result.is_err());
}
#[test]
fn test_quiet_success() {
let result = with_quiet_panic(|| 42);
assert_eq!(result, Ok(42));
}
#[test]
fn test_catch_unwind_basics() {
// catch_unwind returns Result<T, Box<dyn Any>>
let ok = std::panic::catch_unwind(|| 42);
assert_eq!(ok.unwrap(), 42);
let err = std::panic::catch_unwind(|| -> i64 { panic!("boom") });
assert!(err.is_err());
}
}
Deep Comparison
Recover from Panic — Comparison
Core Insight
OCaml's try/with is everyday error handling; Rust's catch_unwind is an escape hatch. This reflects their different philosophies on exceptions vs typed errors.
OCaml Approach
try expr with pattern -> handler — standard, idiomaticFun.protect ~finally for cleanup (like try/finally)Rust Approach
std::panic::catch_unwind converts panic to Result<T, Box<dyn Any>>UnwindSafe bound (or AssertUnwindSafe wrapper)abort mode)Comparison Table
| Aspect | OCaml try/with | Rust catch_unwind |
|---|---|---|
| Idiomacy | Standard practice | Last resort |
| Overhead | Near zero | Stack unwinding |
| Type safety | Pattern matching | Box<dyn Any> downcast |
| Cleanup | Fun.protect ~finally | Drop trait (RAII) |
| Abort mode | N/A | Panics can't be caught |
| Use case | Normal error handling | FFI / thread isolation |
Exercises
run_plugins(plugins: Vec<Box<dyn Fn() -> i32>>) -> Vec<Result<i32, String>> function that runs each plugin in catch_unwind and collects results.std::panic::set_hook and take_hook to capture the panic message (file, line, column) into a string without printing it to stderr.catch_unwind cannot catch panics that cross an FFI boundary by writing a extern "C" function that calls a panicking Rust function.