โข 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
fn compute(a_str: &str, b_str: &str) -> Result<u32, AppError> {
match parse_positive(a_str) {
Ok(a) => match parse_positive(b_str) {
Ok(b) => match safe_div(a, b) {
Ok(result) => Ok(result * 2),
Err(e) => Err(e),
},
Err(e) => Err(e),
},
Err(e) => Err(e),
}
}
This is six lines of boilerplate that all say the same thing: "if it failed, pass the error up." Multiply that by every function in a real application and you have hundreds of lines of noise hiding your actual logic.
The `?` operator collapses all that into a single character. Place `?` after any expression that returns `Result` or `Option`, and Rust does the match for you: if it's `Ok(x)`, unwrap to `x` and continue; if it's `Err(e)`, return `Err(e.into())` immediately. Your function reads like a happy-path-only description of what it does.
use std::num::ParseIntError;
#[derive(Debug)]
enum AppError {
Parse(ParseIntError),
DivByZero,
NegativeInput,
}
// This impl lets ? automatically convert ParseIntError into AppError
impl From<ParseIntError> for AppError {
fn from(e: ParseIntError) -> Self { AppError::Parse(e) }
}
fn parse_positive(s: &str) -> Result<u32, AppError> {
let n: i32 = s.parse()?; // ? here: if parse fails, return Err(AppError::Parse(...))
// The From impl does the conversion automatically
if n < 0 {
Err(AppError::NegativeInput)
} else {
Ok(n as u32)
}
}
fn safe_div(a: u32, b: u32) -> Result<u32, AppError> {
if b == 0 { Err(AppError::DivByZero) } else { Ok(a / b) }
}
// Three ? operators, three potential early returns โ reads like straight-line code
fn compute(a_str: &str, b_str: &str) -> Result<u32, AppError> {
let a = parse_positive(a_str)?; // if this fails, return immediately
let b = parse_positive(b_str)?; // if this fails, return immediately
let result = safe_div(a, b)?; // if this fails, return immediately
Ok(result * 2) // only get here if all three succeeded
}
// ? works on Option too
fn find_double(v: &[i32], target: i32) -> Option<i32> {
let idx = v.iter().position(|&x| x == target)?; // None if not found
let val = v.get(idx)?; // None if out of bounds
Some(val * 2)
}
What `?` desugars to under the hood:
// `expr?` expands to roughly:
match expr {
Ok(val) => val,
Err(e) => return Err(e.into()), // .into() uses the From trait
}
The `.into()` is what makes `?` so flexible: as long as you implement `From<SourceError> for YourError`, `?` handles the conversion automatically.
| Concept | OCaml | Rust |
|---|---|---|
| Error propagation | `let* x = result in ...` (ppx_let) | `let x = result?;` |
| Desugars to | `Result.bind` | `match + return Err(e.into())` |
| Type conversion | Manual | Automatic via `From` trait |
| On Option | Manual `match` or `Option.bind` | `option?` โ returns `None` early |
| Works in closures | Yes | Limited โ `?` only works in functions returning `Result`/`Option` |
//! 293. ? operator and early returns
//!
//! `?` desugars to match + return Err(e.into()), enabling clean error propagation.
use std::num::ParseIntError;
use std::fmt;
#[derive(Debug)]
enum AppError {
Parse(ParseIntError),
DivByZero,
NegativeInput,
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AppError::Parse(e) => write!(f, "parse error: {}", e),
AppError::DivByZero => write!(f, "division by zero"),
AppError::NegativeInput => write!(f, "negative input"),
}
}
}
impl From<ParseIntError> for AppError {
fn from(e: ParseIntError) -> Self {
AppError::Parse(e)
}
}
fn parse_positive(s: &str) -> Result<u32, AppError> {
let n: i32 = s.parse()?; // ? auto-converts ParseIntError via From
if n < 0 {
Err(AppError::NegativeInput)
} else {
Ok(n as u32)
}
}
fn safe_div(a: u32, b: u32) -> Result<u32, AppError> {
if b == 0 { Err(AppError::DivByZero) } else { Ok(a / b) }
}
// Chain of ? operators
fn compute(a_str: &str, b_str: &str) -> Result<u32, AppError> {
let a = parse_positive(a_str)?;
let b = parse_positive(b_str)?;
let result = safe_div(a, b)?;
Ok(result * 2)
}
// ? on Option
fn find_double(v: &[i32], target: i32) -> Option<i32> {
let idx = v.iter().position(|&x| x == target)?;
let val = v.get(idx)?;
Some(val * 2)
}
fn main() {
println!("{:?}", compute("20", "4")); // Ok(10)
println!("{:?}", compute("abc", "4")); // Err(Parse(...))
println!("{:?}", compute("20", "0")); // Err(DivByZero)
println!("{:?}", compute("-5", "2")); // Err(NegativeInput)
let v = [1, 2, 3, 4, 5];
println!("{:?}", find_double(&v, 3)); // Some(6)
println!("{:?}", find_double(&v, 99)); // None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_compute_success() {
assert_eq!(compute("20", "4"), Ok(10));
}
#[test]
fn test_compute_parse_error() {
assert!(matches!(compute("abc", "4"), Err(AppError::Parse(_))));
}
#[test]
fn test_compute_div_zero() {
assert!(matches!(compute("20", "0"), Err(AppError::DivByZero)));
}
#[test]
fn test_question_mark_option() {
let v = [1i32, 2, 3];
assert_eq!(find_double(&v, 2), Some(4));
assert_eq!(find_double(&v, 9), None);
}
}
(* 293. ? operator and early returns - OCaml *)
(* OCaml: use let* with Result monad or explicit bind *)
let ( let* ) = Result.bind
let parse_and_add s1 s2 =
let* a = match int_of_string_opt s1 with
| Some n -> Ok n | None -> Error ("bad: " ^ s1) in
let* b = match int_of_string_opt s2 with
| Some n -> Ok n | None -> Error ("bad: " ^ s2) in
Ok (a + b)
let () =
(match parse_and_add "10" "20" with
| Ok n -> Printf.printf "Sum: %d\n" n
| Error e -> Printf.printf "Error: %s\n" e);
(match parse_and_add "10" "abc" with
| Ok n -> Printf.printf "Sum: %d\n" n
| Error e -> Printf.printf "Error: %s\n" e);
(* Option version *)
let ( let* ) = Option.bind in
let lookup env key =
let* value = List.assoc_opt key env in
let* n = int_of_string_opt value in
Some (n * 2)
in
let env = [("x", "5"); ("y", "bad")] in
(match lookup env "x" with Some n -> Printf.printf "x*2=%d\n" n | None -> print_endline "None");
(match lookup env "y" with Some n -> Printf.printf "y*2=%d\n" n | None -> print_endline "None")