Build the mental model
Most languages like Java, Python, or JavaScript handle recoverable errors (a missing file, a network timeout) by throwing exceptions caught with try/catch — the problem with that approach is a function's signature gives no clue which exceptions it might throw, which can surprise the caller. Rust instead extends Lesson 10's `Option<T>` pattern with `Result<T, E>` (`Ok(T)` for success, `Err(E)` for an error), making the possibility of failure explicit right in the function signature — reading `fn read_file(path: &str) -> Result<String, io::Error>` tells you instantly this function can fail. The `?` operator is concise syntax sugar for propagating errors: `let content = read_file(path)?;` unwraps the value if it's `Ok(value)`, or immediately returns `Err(e)` from the current function if it's `Err(e)` — letting error paths flow through a single line instead of requiring a nested match in every function. Rust's philosophy sharply separates two error categories: recoverable errors (`Result`) and unrecoverable errors (`panic!`, covered in more depth in Lesson 20) — a function's type signature itself documents exactly how it can fail.
Connect it to a real scenario
Write the content analyzer's file-reading function as `fn read_lesson(path: &str) -> Result<String, io::Error>` — if the file is missing or a permission issue occurs, the caller is compile-time forced to handle it. In a function scanning a whole directory and processing each file, call `read_lesson(path)?` per file — if one file fails, the whole batch immediately aborts and the error propagates upstream. In a report-generation function, `match read_lesson(path) { Ok(content) => ..., Err(e) => eprintln!("skip {path}: {e}") }` lets you decide to keep processing even if one file fails — choosing between `?` (abort) and explicit `match` (continue) is a real design decision.
Try the working example
use std::fs;
use std::io;
fn read_lesson(path: &str) -> Result<String, io::Error> {
let content = fs::read_to_string(path)?;
Ok(content)
}
fn word_count(path: &str) -> Result<usize, io::Error> {
let content = read_lesson(path)?;
Ok(content.split_whitespace().count())
}
fn main() {
match word_count("lessons/ownership.md") {
Ok(count) => println!("{count} words"),
Err(e) => eprintln!("failed to read lesson: {e}"),
}
}If the file exists you print its word count; if it doesn't, an error message prints to stderr — either way the program does not crash.5-minute try-it
Write `fn parse_difficulty(s: &str) -> Result<u32, String>` that compares the input against "basic"/"intermediate"/"advanced" and returns `Ok` with a numeric level (1/2/3) on a match, or a descriptive `Err(String)` otherwise — test it with a caller function that uses the `?` operator.
One important caution
Using `.unwrap()` on a `Result` in production code without handling the error case — even predictable errors like "file not found" or "permission denied" then crash the program with a panic.
Trying to use the `?` operator inside a function whose return type isn't `Result` (like a plain `fn main()`) — `?` only works when the current function's return type is compatible with the error type, otherwise it's a compile error.
The Rust Programming Language — Recoverable Errors with Result — Rust