ExamplesBy LevelBy TopicLearning Paths
1014 Intermediate

1014-recover-from-panic — Recover from Panic

Functional Programming

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

  • • Use std::panic::catch_unwind to convert a panic into a Result
  • • Understand UnwindSafe and why mutable references require AssertUnwindSafe
  • • Extract the panic payload from the returned Box<dyn Any> using downcast_ref
  • • Know that catch_unwind does not work with panic = "abort" builds
  • • Understand the right contexts for using this API (plugin isolation, test harnesses, FFI)
  • Code 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

  • First-class vs escape hatch: OCaml 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: Rust requires types crossing the unwind boundary to be UnwindSafe to prevent unsound state; OCaml has no such restriction.
  • Panic payload typing: Rust panics can carry any '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());
        }
    }
    ✓ Tests Rust test suite
    #[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, 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/withRust catch_unwind
    IdiomacyStandard practiceLast resort
    OverheadNear zeroStack unwinding
    Type safetyPattern matchingBox<dyn Any> downcast
    CleanupFun.protect ~finallyDrop trait (RAII)
    Abort modeN/APanics can't be caught
    Use caseNormal error handlingFFI / thread isolation

    Exercises

  • Write a run_plugins(plugins: Vec<Box<dyn Fn() -> i32>>) -> Vec<Result<i32, String>> function that runs each plugin in catch_unwind and collects results.
  • Use std::panic::set_hook and take_hook to capture the panic message (file, line, column) into a string without printing it to stderr.
  • Demonstrate that catch_unwind cannot catch panics that cross an FFI boundary by writing a extern "C" function that calls a panicking Rust function.
  • Open Source Repos