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?
- A. Stores the null character in
initial. - B. Panics because the string is empty.
- C. Rejects
Option<char>wherecharis required. - D. Converts
Noneto a space.
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.