308: When to Panic vs Return Result
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
Result) from programming bugs (use panic! or assert!)assert!() and assert_eq!() to document and enforce invariantsResult from library functions that receive user-controlled inputCode 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! unwinds the thread; OCaml exceptions are catchable with try/with — Rust panics can be caught with catch_unwind but this is uncommon.panic documents "this is a bug" in the library contract; Result documents "this can fail at runtime".#[should_panic] tests that code panics on contract violations; assert_eq! tests that Results contain expected values.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);
}
}#[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
safe_sqrt(x: f64) -> Result<f64, String> and a sqrt_positive(x: f64) -> f64 (panics on negative) — document when each is appropriate.Result, and show that callers must now handle the error.assert! precondition checks to an internal function, then write a test with #[should_panic] that verifies the assertion fires on invalid input.