← Back to Rust series
🦀
Error handling
Error handling · Prerequisite: types 1-4 · Error handling · Prerequisite: lesson 15

15–16. Working with Result<T,E> and Option<T> · The ? Operator and Error Propagation

Rust's error handling boils down to two enums — `Option<T>` for "may not exist" and `Result<T,E>` for "may fail." There's no try/catch; failure is encoded into types, so the compiler catches forgotten handling. This lesson covers conversions and helper methods between the two. The `?` operator after a Result or Option expression means "return on failure immediately," turning nested match into a single line. Combined with the `From` trait it auto-converts error types as it propagates up the call stack.

RustResultOptionerror handlingmatch?error propagationFrom
Duration
⏱ ~1.5 hours + ~1.5 hours
Level
📊 Intermediate
Prerequisite
🎯 Types 1-4
OUTCOME
Rust's error handling boils down to two enums — `Option<T>` for "may not exist" and `Result<T,E>` for "may fail." There's no try/catch; failure is encoded into types, so the compiler catches forgotten handling. This lesson covers conversions and helper methods between the two. / The `?` operator after a Result or Option expression means "return on failure immediately," turning nested match into a single line. Combined with the `From` trait it auto-converts error types as it propagates up the call stack.

What you'll learn

  • 1Tell apart Result<T,E> and Option<T>
  • 2Branch with match or helper methods
  • 3Convert with `.ok()` / `.ok_or()`
  • 4Mark fallible functions in their signature
  • 5Know when to panic vs. return Result
  • 6Propagate Result/Option failures with `?`
  • 7Make main return Result so `?` works there
  • 8Wire automatic error conversion via the From trait
  • 9Know when `?` works on Option too
  • 10Use Box<dyn Error> for quick error aggregation

15. Working with Result<T,E> and Option<T>

Rust's error handling boils down to two enums — `Option<T>` for "may not exist" and `Result<T,E>` for "may fail." There's no try/catch; failure is encoded into types, so the compiler catches forgotten handling. This lesson covers conversions and helper methods between the two.

Overview

Other languages hide failure either invisibly (unchecked exceptions) or with a separate throws clause. Rust puts it on the return type — the caller sees how a function can fail at a glance, and missing the handling is a compile error.

Core Concepts

1) Result<T, E>

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

Carries both success type T and failure type E in the signature; both can hold data.

2) Option<T> vs Result<T,E>

AspectOptionResult
Purposemay / may-not existsuccess / failure with detail
AbsenceNone (no info)Err(E) (with reason)
When to uselist lookup, optional fieldI/O, network, parsing

3) Common helpers

MethodBehavior
.unwrap()Ok/Some → inner, else panic
.expect("msg")panic with message
.unwrap_or(d)default on failure
.unwrap_or_else(|e| ...)computed default
.ok()Result → Option (discards Err)
.map(|x| ...)transform on Ok/Some
.map_err(|e| ...)transform Err only

Hands-on Examples

Parsing with a Result return:

rust
fn parse_age(s: &str) -> Result<u32, String> {
    match s.trim().parse::<u32>() {
        Ok(n) if n > 150 => Err(format!("unrealistic age: {}", n)),
        Ok(n)            => Ok(n),
        Err(e)           => Err(format!("parse failed: {}", e)),
    }
}

fn main() {
    for s in ["30", "abc", "999"] {
        match parse_age(s) {
            Ok(n)  => println!("{} OK -> {}", s, n),
            Err(e) => println!("{} ERR -> {}", s, e),
        }
    }
}

Option ↔ Result:

rust
fn find(v: &[i32], target: i32) -> Option<usize> {
    v.iter().position(|&x| x == target)
}

fn find_or_err(v: &[i32], target: i32) -> Result<usize, String> {
    find(v, target).ok_or(format!("{} not found", target))
}

fn main() {
    let v = [10, 20, 30];
    println!("{:?}", find_or_err(&v, 20));  // Ok(1)
    println!("{:?}", find_or_err(&v, 99));  // Err("99 not found")
}

Helper chaining:

rust
fn main() {
    let s = "  42  ";
    let n = s.trim().parse::<i32>()
        .map(|x| x * 2)
        .unwrap_or(-1);
    println!("{}", n); // 84
}

Common Mistakes

Q. Do I really write match every time?

A. No — the next lesson's `?` operator reduces "fail-and-return" to one character. `.map` / `.and_then` chains cover most of the rest.

Q. Can I just .unwrap()?

A. OK for prototypes / examples / tests. In production code only when **"this can never be None/Err" is structurally obvious**. Never on user input or I/O results.

Q. Option or Result?

A. **If the reason for failure is meaningful, Result. If it's pure absence, Option.** Example: list lookup → Option, file open → Result.

Recap

  • Option = absence; Result = failure with reason
  • Fallible functions spell out their error type → forgotten handling is a compile error
  • Branch with match, .unwrap_or, .map, .ok_or, etc.
  • Use .unwrap() carefully

Try It Yourself

  1. Write `fn divide(a: i32, b: i32) -> Result<i32, String>` (Err when b=0)
  2. Parse a user-entered string as u32, accept only 1~120 (Err otherwise)
  3. Collect Ok values from `Vec<Result<i32, String>>` and sum them

16. The ? Operator and Error Propagation

The `?` operator after a Result or Option expression means "return on failure immediately," turning nested match into a single line. Combined with the `From` trait it auto-converts error types as it propagates up the call stack.

Overview

Match-on-every-step turns into a pyramid of indentation. The `?` character flattens that — "if this step fails, just return upward as is."

Core Concepts

1) ? on a Result

`expr?` is equivalent to:

rust
// expr?
// is shorthand for:
match expr {
    Ok(v)  => v,
    Err(e) => return Err(e.into()), // From-converted
}

2) ? on an Option

If the enclosing function returns Option, `?` returns None immediately on None.

3) Auto-conversion via From

If your error type `MyError` implements `From<io::Error>`, then `?` on an io operation auto-converts to `MyError`. This is how you unify several error types into one.

4) main can return Result too

rust
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
    let n = "42".parse::<i32>()?;
    println!("{}", n);
    Ok(())
}

Hands-on Examples

Open file and parse first line — without and with `?`:

rust
use std::fs::File;
use std::io::{self, Read};

// without ? — pyramid of indentation
fn read_first_number_v1(path: &str) -> Result<i32, String> {
    let mut f = match File::open(path) {
        Ok(f) => f,
        Err(e) => return Err(format!("open: {}", e)),
    };
    let mut s = String::new();
    if let Err(e) = f.read_to_string(&mut s) {
        return Err(format!("read: {}", e));
    }
    let first = s.lines().next().ok_or("empty")?;
    first.trim().parse::<i32>().map_err(|e| format!("parse: {}", e))
}
rust
// with ? and Box<dyn Error> — line by line
use std::error::Error;

fn read_first_number_v2(path: &str) -> Result<i32, Box<dyn Error>> {
    let mut s = String::new();
    File::open(path)?.read_to_string(&mut s)?;
    let first = s.lines().next().ok_or("empty")?;
    let n = first.trim().parse::<i32>()?;
    Ok(n)
}

With Option:

rust
fn first_char_uppercased(s: &str) -> Option<char> {
    let c = s.chars().next()?;          // None → return None
    c.to_uppercase().next()
}

fn main() {
    println!("{:?}", first_char_uppercased("hello")); // Some('H')
    println!("{:?}", first_char_uppercased(""));      // None
}

Common Mistakes

Q. Does `?` only work inside a function?

A. Yes, and that function must return Result or Option. main can if you give it `Result<(), Box<dyn Error>>`. Outside of fallible contexts `?` is a compile error.

Q. The error types differ — does `?` still work?

A. Yes, as long as `From` is implemented between them. `Box<dyn Error>` is the catch-all type that accepts almost any error (handy for binaries; libraries should prefer concrete error types like `thiserror`).

Q. Can I use `?` in the middle of an expression?

A. Yes: `let n = parse()? + 1;`. Just needs to be in a place where the operand has type Result or Option.

Recap

  • `?` = single-character "return on failure"
  • Function must return Result or Option to use it
  • From-impl provides automatic error conversion
  • main returning Result lets you use `?` at the top level

Try It Yourself

  1. Two-step parse (string → int → positive check) using `?`
  2. Write `fn first_two_uppercased(s: &str) -> Option<(char, char)>` with `?`
  3. Refactor main to `-> Result<(), Box<dyn Error>>` and propagate file IO + parsing errors with `?`
Example code / lecture materials

All lecture materials and example code are openly available on GitHub.

View on GitHub ↗

Comments 0

No comments yet. Be the first.

Only members can comment.

Comments with ads, abuse, personal information or illegal content are hidden or removed. Comments are shown with your nickname. Privacy Policy