ExamplesBy LevelBy TopicLearning Paths
308 Intermediate

308: When to Panic vs Return Result

Functional Programming

Tutorial Video

Text description (accessibility)

This video demonstrates the "308: When to Panic vs Return Result" functional Rust example. Difficulty level: Intermediate. Key concepts covered: Functional Programming. The choice between `panic!()` and returning `Result` is architectural. Key difference from OCaml: 1. **Panic = unrecoverable**: Rust's `panic!` unwinds the thread; OCaml exceptions are catchable with `try/with` — Rust panics can be caught with `catch_unwind` but this is uncommon.

Tutorial

The Problem

The choice between panic!() and returning Result is architectural. Panic for programming errors (bugs the developer should fix); return Result for operational errors (bad user input, network failures, missing files). Getting this wrong creates brittle APIs that crash on recoverable failures, or obscure programmer errors behind error types that callers don't know how to handle. This distinction maps to OCaml's choice between exceptions and Result.

🎯 Learning Outcomes

  • • Distinguish recoverable errors (return Result) from programming bugs (use panic! or assert!)
  • • Use assert!() and assert_eq!() to document and enforce invariants
  • • Return Result from library functions that receive user-controlled input
  • • Panic for impossible states that indicate a bug: violated post-conditions, index out of bounds on internal data
  • Code Example

    #![allow(clippy::all)]
    //! # When to Panic vs Return Result
    //!
    //! Result for recoverable errors, panic for programming bugs.
    
    /// Library function: user provides invalid input -> use Result
    pub fn parse_age(s: &str) -> Result<u8, String> {
        let n: i32 = s.parse().map_err(|_| format!("'{}' is not a number", s))?;
        if n < 0 || n > 150 {
            return Err(format!("age {} is out of range [0, 150]", n));
        }
        Ok(n as u8)
    }
    
    /// Internal: programmer error -> can panic
    pub fn get_element<T>(arr: &[T], index: usize) -> &T {
        &arr[index] // panics if out of bounds
    }
    
    /// Invariant that must always hold
    pub fn divide(a: i32, b: i32) -> i32 {
        assert!(b != 0, "divide: b must not be zero");
        a / b
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_parse_age_valid() {
            assert_eq!(parse_age("25"), Ok(25));
            assert_eq!(parse_age("0"), Ok(0));
            assert_eq!(parse_age("150"), Ok(150));
        }
    
        #[test]
        fn test_parse_age_invalid() {
            assert!(parse_age("abc").is_err());
            assert!(parse_age("200").is_err());
            assert!(parse_age("-1").is_err());
        }
    
        #[test]
        fn test_get_element() {
            let arr = [10i32, 20, 30];
            assert_eq!(*get_element(&arr, 1), 20);
        }
    
        #[test]
        #[should_panic]
        fn test_get_element_panics() {
            let arr = [1i32];
            get_element(&arr, 99);
        }
    
        #[test]
        fn test_divide() {
            assert_eq!(divide(10, 2), 5);
        }
    
        #[test]
        #[should_panic]
        fn test_divide_by_zero_panics() {
            divide(10, 0);
        }
    }

    Key Differences

  • Panic = unrecoverable: Rust's panic! unwinds the thread; OCaml exceptions are catchable with try/with — Rust panics can be caught with catch_unwind but this is uncommon.
  • API contract: panic documents "this is a bug" in the library contract; Result documents "this can fail at runtime".
  • Testing: #[should_panic] tests that code panics on contract violations; assert_eq! tests that Results contain expected values.
  • Infallible: std::convert::Infallible and the ! type represent operations that cannot fail — the type system enforces it.
  • OCaml Approach

    OCaml uses exceptions for panic-equivalent cases and Result/Option for expected failures:

    (* User input: use Result *)
    let parse_age s =
      match int_of_string_opt s with
      | None -> Error (Printf.sprintf "'%s' is not a number" s)
      | Some n when n < 0 || n > 150 -> Error "age out of range"
      | Some n -> Ok n
    
    (* Programmer invariant: raise Invalid_argument *)
    let divide a b =
      if b = 0 then raise (Invalid_argument "divide by zero")
      else a / b
    

    Full Source

    #![allow(clippy::all)]
    //! # When to Panic vs Return Result
    //!
    //! Result for recoverable errors, panic for programming bugs.
    
    /// Library function: user provides invalid input -> use Result
    pub fn parse_age(s: &str) -> Result<u8, String> {
        let n: i32 = s.parse().map_err(|_| format!("'{}' is not a number", s))?;
        if n < 0 || n > 150 {
            return Err(format!("age {} is out of range [0, 150]", n));
        }
        Ok(n as u8)
    }
    
    /// Internal: programmer error -> can panic
    pub fn get_element<T>(arr: &[T], index: usize) -> &T {
        &arr[index] // panics if out of bounds
    }
    
    /// Invariant that must always hold
    pub fn divide(a: i32, b: i32) -> i32 {
        assert!(b != 0, "divide: b must not be zero");
        a / b
    }
    
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_parse_age_valid() {
            assert_eq!(parse_age("25"), Ok(25));
            assert_eq!(parse_age("0"), Ok(0));
            assert_eq!(parse_age("150"), Ok(150));
        }
    
        #[test]
        fn test_parse_age_invalid() {
            assert!(parse_age("abc").is_err());
            assert!(parse_age("200").is_err());
            assert!(parse_age("-1").is_err());
        }
    
        #[test]
        fn test_get_element() {
            let arr = [10i32, 20, 30];
            assert_eq!(*get_element(&arr, 1), 20);
        }
    
        #[test]
        #[should_panic]
        fn test_get_element_panics() {
            let arr = [1i32];
            get_element(&arr, 99);
        }
    
        #[test]
        fn test_divide() {
            assert_eq!(divide(10, 2), 5);
        }
    
        #[test]
        #[should_panic]
        fn test_divide_by_zero_panics() {
            divide(10, 0);
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_parse_age_valid() {
            assert_eq!(parse_age("25"), Ok(25));
            assert_eq!(parse_age("0"), Ok(0));
            assert_eq!(parse_age("150"), Ok(150));
        }
    
        #[test]
        fn test_parse_age_invalid() {
            assert!(parse_age("abc").is_err());
            assert!(parse_age("200").is_err());
            assert!(parse_age("-1").is_err());
        }
    
        #[test]
        fn test_get_element() {
            let arr = [10i32, 20, 30];
            assert_eq!(*get_element(&arr, 1), 20);
        }
    
        #[test]
        #[should_panic]
        fn test_get_element_panics() {
            let arr = [1i32];
            get_element(&arr, 99);
        }
    
        #[test]
        fn test_divide() {
            assert_eq!(divide(10, 2), 5);
        }
    
        #[test]
        #[should_panic]
        fn test_divide_by_zero_panics() {
            divide(10, 0);
        }
    }

    Deep Comparison

    panic-vs-result

    See README.md for details.

    Exercises

  • Write a safe_sqrt(x: f64) -> Result<f64, String> and a sqrt_positive(x: f64) -> f64 (panics on negative) — document when each is appropriate.
  • Refactor a function that currently panics on invalid input to return Result, and show that callers must now handle the error.
  • Add assert! precondition checks to an internal function, then write a test with #[should_panic] that verifies the assertion fires on invalid input.
  • Open Source Repos