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.
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>
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
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:
// 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
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 `?`:
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))
}// 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:
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
- Two-step parse (string → int → positive check) using `?`
- Write `fn first_two_uppercased(s: &str) -> Option<(char, char)>` with `?`
- Refactor main to `-> Result<(), Box<dyn Error>>` and propagate file IO + parsing errors with `?`
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