๐Ÿฆ€ Functional Rust

1014: Recover from Panic

Difficulty: Advanced Category: Error Handling Concept: Using `std::panic::catch_unwind` to recover from panics and convert them to Results Key Insight: `catch_unwind` is Rust's equivalent of OCaml's `try/with` for exceptions โ€” but it's a last resort in Rust, mainly for FFI boundaries and thread isolation, not normal control flow.
// 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())
    })
}

fn main() {
    match safe_divide(10, 0) {
        Ok(v) => println!("Result: {}", v),
        Err(e) => println!("Caught panic: {}", e),
    }

    match safe_divide(10, 2) {
        Ok(v) => println!("Result: {}", v),
        Err(e) => println!("Error: {}", e),
    }

    println!("Run `cargo test` to verify all examples.");
}

#[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());
    }
}
(* 1014: Recover from Panic *)
(* OCaml try/with for catching exceptions *)

(* Approach 1: try/with โ€” catch specific exceptions *)
let risky_divide a b =
  if b = 0 then failwith "division by zero"
  else a / b

let safe_divide a b =
  try Ok (risky_divide a b)
  with Failure msg -> Error msg

(* Approach 2: Catch all exceptions *)
let catch_all f =
  try Ok (f ())
  with
  | Failure msg -> Error (Printf.sprintf "Failure: %s" msg)
  | Invalid_argument msg -> Error (Printf.sprintf "Invalid: %s" msg)
  | exn -> Error (Printf.sprintf "Unknown: %s" (Printexc.to_string exn))

(* Approach 3: Using Fun.protect for cleanup *)
let with_resource f =
  let resource = "opened" in
  Fun.protect
    ~finally:(fun () -> Printf.printf "    cleanup: resource closed\n")
    (fun () -> f resource)

let test_try_with () =
  assert (safe_divide 10 2 = Ok 5);
  (match safe_divide 10 0 with
   | Error msg -> assert (msg = "division by zero")
   | Ok _ -> assert false);
  Printf.printf "  Approach 1 (try/with): passed\n"

let test_catch_all () =
  assert (catch_all (fun () -> 42) = Ok 42);
  (match catch_all (fun () -> failwith "boom") with
   | Error msg -> assert (String.length msg > 0)
   | Ok _ -> assert false);
  (match catch_all (fun () -> invalid_arg "bad") with
   | Error msg -> assert (String.length msg > 0)
   | Ok _ -> assert false);
  Printf.printf "  Approach 2 (catch all): passed\n"

let test_protect () =
  let result = try Ok (with_resource (fun r -> String.length r))
               with _ -> Error "failed" in
  assert (result = Ok 6);
  Printf.printf "  Approach 3 (Fun.protect): passed\n"

let () =
  Printf.printf "Testing recover from panic:\n";
  test_try_with ();
  test_catch_all ();
  test_protect ();
  Printf.printf "โœ“ All tests passed\n"

๐Ÿ“Š Detailed 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, idiomatic
  • Can catch specific exceptions or all with wildcard
  • `Fun.protect ~finally` for cleanup (like try/finally)
  • Exceptions are cheap and common in OCaml

Rust Approach

  • `std::panic::catch_unwind` converts panic to `Result<T, Box<dyn Any>>`
  • Requires `UnwindSafe` bound (or `AssertUnwindSafe` wrapper)
  • Only catches unwinding panics (not `abort` mode)
  • Intended for FFI boundaries, thread pools, not normal flow

Comparison Table

AspectOCaml `try/with`Rust `catch_unwind`
IdiomacyStandard practiceLast resort
OverheadNear zeroStack unwinding
Type safetyPattern matching`Box<dyn Any>` downcast
Cleanup`Fun.protect ~finally``Drop` trait (RAII)
Abort modeN/APanics can't be caught
Use caseNormal error handlingFFI / thread isolation