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.
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
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>
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>
| Aspect | Option | Result |
|---|---|---|
| Purpose | may / may-not exist | success / failure with detail |
| Absence | None (no info) | Err(E) (with reason) |
| When to use | list lookup, optional field | I/O, network, parsing |
3) Common helpers
| Method | Behavior |
|---|---|
| .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:
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:
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:
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
- Write `fn divide(a: i32, b: i32) -> Result<i32, String>` (Err when b=0)
- Parse a user-entered string as u32, accept only 1~120 (Err otherwise)
- Collect Ok values from `Vec<Result<i32, String>>` and sum them
All lecture materials and example code are openly available on GitHub.
View on GitHub ↗