โข Option
โข Result
โข The ? operator propagates errors up the call stack concisely
โข Combinators like .map(), .and_then(), .unwrap_or() chain fallible operations
โข The compiler forces you to handle every error case โ no silent failures
โข Option
โข Result
โข The ? operator propagates errors up the call stack concisely
โข Combinators like .map(), .and_then(), .unwrap_or() chain fallible operations
โข The compiler forces you to handle every error case โ no silent failures
let a = match parse_int(a_str) {
Ok(v) => v,
Err(e) => return Err(e),
};
let b = match parse_int(b_str) {
Ok(v) => v,
Err(e) => return Err(e),
};
That's 10 lines of boilerplate for two fallible calls. With five steps, you've written 25 lines of match arms that all do the same thing: "if error, return it." The actual logic โ what you're computing โ is drowned out.
Java's checked exceptions were an attempt to solve this, but callers still have to write `throws` declarations everywhere or wrap everything in `RuntimeException`. Python's exceptions propagate automatically, but that means they're also invisible โ you can't tell from reading code which calls might throw. Rust finds the middle ground: errors propagate explicitly with `?`, but the propagation is opt-in, visible, and checked by the compiler.
// Implementing From lets ? auto-convert error types
impl From<ParseIntError> for AppError {
fn from(e: ParseIntError) -> Self { AppError::Parse(e) }
}
fn parse_int(s: &str) -> Result<i64, AppError> {
// The inner parse returns ParseIntError; ? converts it to AppError via From
Ok(s.trim().parse::<i64>()?)
}
// Multi-step computation using ? at each step
fn compute(a_str: &str, b_str: &str) -> Result<i64, AppError> {
let a = parse_int(a_str)?; // if Err(AppError::Parse), return immediately
let b = parse_int(b_str)?; // same
let quotient = safe_div(a, b)?; // if Err(AppError::DivByZero), return immediately
let validated = validate_range(quotient)?; // same
Ok(validated * 2) // only reached if all steps succeeded
}
// Compare: WITHOUT ? operator (5 steps, 25 lines)
fn compute_verbose(a_str: &str, b_str: &str) -> Result<i64, AppError> {
let a = match parse_int(a_str) { Ok(v) => v, Err(e) => return Err(e) };
let b = match parse_int(b_str) { Ok(v) => v, Err(e) => return Err(e) };
// ... three more of these
}
// With ?: 5 lines. Without?: 15+ lines. Same behavior.
| Concept | OCaml | Rust | ||
|---|---|---|---|---|
| Propagate error | `let* x = r in ...` (binding operator) | `let x = r?;` | ||
| Manual early return | `match r with Error e -> Error e \ | Ok v -> ...` | `match r { Err(e) => return Err(e), Ok(v) => ... }` | |
| Error type conversion | Manual wrapping or `Result.map_error` | Automatic via `From<E>` trait + `?` | ||
| Where it works | Any expression | Only in functions returning `Result` or `Option` | ||
| `main` function | Returns `unit`, handle errors manually | `fn main() -> Result<(), E>` โ `?` works in main too | ||
| Chained context | `Result.map_error (fun e -> ...)` | `.map_err(\ | e\ | ...)` before `?` |
// Error Propagation โ 99 Problems #48
// Use the ? operator to propagate errors through call stacks.
use std::num::ParseIntError;
#[derive(Debug)]
enum AppError {
Parse(ParseIntError),
DivByZero,
ValidationFailed(String),
}
impl std::fmt::Display for AppError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AppError::Parse(e) => write!(f, "parse error: {}", e),
AppError::DivByZero => write!(f, "division by zero"),
AppError::ValidationFailed(msg) => write!(f, "validation failed: {}", msg),
}
}
}
impl From<ParseIntError> for AppError {
fn from(e: ParseIntError) -> Self {
AppError::Parse(e)
}
}
fn parse_int(s: &str) -> Result<i64, AppError> {
// Using From<ParseIntError> via ? conversion
Ok(s.trim().parse::<i64>()?)
}
fn safe_div(a: i64, b: i64) -> Result<i64, AppError> {
if b == 0 { Err(AppError::DivByZero) } else { Ok(a / b) }
}
fn validate_range(x: i64) -> Result<i64, AppError> {
if x > 0 && x < 10000 {
Ok(x)
} else {
Err(AppError::ValidationFailed(format!("{} not in (0, 10000)", x)))
}
}
/// Multi-step computation using ? at each step.
fn compute(a_str: &str, b_str: &str) -> Result<i64, AppError> {
let a = parse_int(a_str)?;
let b = parse_int(b_str)?;
let quotient = safe_div(a, b)?;
let validated = validate_range(quotient)?;
Ok(validated * 2)
}
/// Process a list, collecting errors with context.
fn process_list(inputs: &[(&str, &str)]) -> Vec<Result<i64, String>> {
inputs
.iter()
.map(|(a, b)| compute(a, b).map_err(|e| format!("({},{}) -> {}", a, b, e)))
.collect()
}
fn main() {
let cases = [
("100", "5"),
("100", "0"),
("abc", "5"),
("50000", "1"),
("50", "2"),
];
println!("Computing results:");
for (a, b) in &cases {
match compute(a, b) {
Ok(v) => println!(" compute({:?}, {:?}) = {}", a, b, v),
Err(e) => println!(" compute({:?}, {:?}) = Error: {}", a, b, e),
}
}
println!("\nProcess list:");
for result in process_list(&cases) {
println!(" {:?}", result);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_success() {
assert_eq!(compute("100", "5"), Ok(40)); // 100/5=20, *2=40
}
#[test]
fn test_div_zero() {
assert!(matches!(compute("100", "0"), Err(AppError::DivByZero)));
}
#[test]
fn test_parse_error() {
assert!(matches!(compute("abc", "5"), Err(AppError::Parse(_))));
}
#[test]
fn test_validation_error() {
assert!(matches!(
compute("50000", "1"),
Err(AppError::ValidationFailed(_))
));
}
#[test]
fn test_from_conversion() {
let result = parse_int("not_a_number");
assert!(matches!(result, Err(AppError::Parse(_))));
}
}
(* Error Propagation *)
(* OCaml 99 Problems #48 *)
(* Implementation for example 48 *)
(* Tests *)
let () =
(* Add tests *)
print_endline "โ OCaml tests passed"