Skip to content
Error Handling: Result and Option
step 1/7

Reading — step 1 of 7

Learn

~2 min readStructs, Enums, and Error Handling

Rust has no exceptions for ordinary errors. Instead, fallible operations return either Result<T, E> or Option<T> — and the compiler forces you to handle both cases. Chapter 9 of the Rust Book makes the case clearly: errors are values, not control flow.

Option<T>

For "the value might be missing":

enum Option<T> {
    Some(T),
    None,
}

fn find_user(id: u32) -> Option<String> {
    if id == 1 {
        Some(String::from("Ada"))
    } else {
        None
    }
}

let name = find_user(1);
match name {
    Some(n) => println!("found {}", n),
    None => println!("not found"),
}

Result<T, E>

For "this might fail with a specific error":

enum Result<T, E> {
    Ok(T),
    Err(E),
}

fn parse_age(s: &str) -> Result<u32, String> {
    s.trim().parse::<u32>()
        .map_err(|_| format!("not a valid age: {}", s))
}

match parse_age("42") {
    Ok(n) => println!("age: {}", n),
    Err(e) => println!("error: {}", e),
}

The ? operator — early-return on Err/None

Manually matching every Result is verbose. The ? operator handles it for you:

fn add_two_inputs() -> Result<i32, std::num::ParseIntError> {
    let mut a = String::new();
    std::io::stdin().read_line(&mut a).unwrap();
    let a: i32 = a.trim().parse()?;       // returns early on Err
    
    let mut b = String::new();
    std::io::stdin().read_line(&mut b).unwrap();
    let b: i32 = b.trim().parse()?;
    
    Ok(a + b)
}

If any ? hits an Err, the function immediately returns that Err. On Ok, it unwraps the value.

? works for both Result and Option — but you can't mix them in one function unless you convert.

Useful helper methods

let x: Option<i32> = Some(5);

x.unwrap();                  // panics on None — use sparingly
x.unwrap_or(0);              // default if None
x.unwrap_or_else(|| compute_default());
x.expect("must be Some");    // panics with custom message
x.is_some();                  // true
x.is_none();                  // false
x.map(|n| n * 2);            // Some(10)
x.and_then(|n| safe_div(100, n));   // chain Option-returning ops

Result has all the same plus error-mapping methods like .map_err().

panic! — when there's no recovering

fn impossible() -> ! {
    panic!("this should never happen");
}

Use panic! for programmer errors (invariants violated, code paths that should be unreachable). For expected failures (parsing, I/O), return Result.

Why this design wins

In languages with exceptions, you can call any function and get a surprise crash from anywhere. In Rust:

  • Every fallible signature says so: Result<T, E> is in the type.
  • The compiler refuses to compile if you ignore an Err.
  • No silent surprises. No "forgot to catch."

The trade-off: more verbose plumbing in error paths. The ? operator was added specifically to make that plumbing cheap.

Common mistakes

  • .unwrap() everywhere — panics propagate; production code should handle errors. Reserve for programmer-checked invariants.
  • ? outside a Result/Option-returning function — compile error.
  • Matching every Result manually when ? would do — verbose. ? is idiomatic.
  • Returning Result<T, &str> for libraries — fine for examples, but real libraries define error enums or use Box<dyn std::error::Error>.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…