R Rust by Evidence PDF

Chapter 8 — Option and Result

Goal

Separate ordinary absence from operations that can fail, without sentinel values or unchecked exceptions.

Option<T> means a T may be absent. Result<T, E> means an operation either produced T or explains failure with E. Both are ordinary enums in the standard library.

fn first_char(text: &str) -> Option<char> {
    text.chars().next()
}

fn main() {
    let initial: char = first_char("");
    println!("{initial}");
}

Prediction

What does the compiler do?

RevealC

Representative evidence:

error[E0308]: mismatched types
  |
  |     let initial: char = first_char("");
  |                  ----   ^^^^^^^^^^^^^^ expected `char`, found `Option<char>`

The type forces the caller to choose what absence means.

Mental model

Working model: Some(value) is present and None is absent. Ok(value) is success and Err(error) is failure. You cannot use the inner value until you handle the outer enum.

Precise model: Option<T> and Result<T, E> compose because their methods transform only the success/present branch. map changes an inner value; and_then chains an operation that already returns the same kind of wrapper. The ? operator returns early on None or Err and unwraps the continuing branch. Its enclosing function must return a compatible type.

fn first_upper(text: &str) -> Option<char> {
    let first = text.chars().next()?;
    Some(first.to_ascii_uppercase())
}

fn parse_port(text: &str) -> Result<u16, std::num::ParseIntError> {
    let port = text.parse::<u16>()?;
    Ok(port)
}

fn main() {
    println!("{:?}", first_upper("rust"));
    println!("{:?}", first_upper(""));
    println!("{:?}", parse_port("8080"));
    println!("{:?}", parse_port("many"));
}

Representative output:

Some('R')
None
Ok(8080)
Err(ParseIntError { kind: InvalidDigit })

The exact debug text of library errors is not a stable interface; match on error values or display them rather than depending on that formatting.

Choose between the wrappers by asking whether the caller needs a reason. Searching a list may naturally return None. Parsing user input should return Err because malformed input differs from successful absence.

Simplest fix

For a genuine default, unwrap explicitly with unwrap_or:

fn first_char(text: &str) -> Option<char> { text.chars().next() }

let initial = first_char("").unwrap_or('?');

For branching behavior, use match or if let. In reusable code, prefer returning the Option or Result so the caller keeps the choice.

Tradeoff alternative: expect("non-empty username") extracts the value and panics on absence. It is fine when absence proves a programmer invariant was broken, and useful in small examples. It is a poor response to routine user input or network failure because it terminates the current thread instead of reporting a recoverable outcome.

Mini challenge

Complete the function without unwrap, expect, or a manual match:

fn doubled(text: &str) -> Result<i32, std::num::ParseIntError> {
    // parse an i32, then double it
    todo!()
}

Answer

fn doubled(text: &str) -> Result<i32, std::num::ParseIntError> {
    let number = text.parse::<i32>()?;
    Ok(number * 2)
}

A shorter alternative is text.parse::<i32>().map(|number| number * 2). The ? version is often easier to extend with another fallible step; map is compact for one transformation.