12.xChapter 12 summary and quiz
This chapter taught Rust's two-track approach to failure and the syntax that makes the recoverable track painless. Review, then a capstone that wires it all together.
Quick review
Rust splits failure in two. Unrecoverable failures are bugs, broken assumptions, and they call for panic! (12.1): it prints a message and location, unwinds the stack (dropping values as it goes), and stops the thread. RUST_BACKTRACE=1 adds the call chain. assert!, assert_eq!, and debug_assert! are conditional panics for checking invariants, and unwrap/expect are convenience methods that panic when there's nothing to unwrap.
Recoverable failures are expected events, and they call for Result<T, E> (12.2), an enum with Ok(T) and Err(E), handled with match like any chapter 11 enum. It's the type hiding inside every parse and read_line you've called; .expect() was treating its Err as a panic all along. Result makes failure a value you inspect, not an interruption, and exhaustiveness means you can't forget the error case.
The ? operator (12.3) is the propagation pattern in one character: expr? unwraps an Ok to its value or returns the Err from the current function. It only works where the function can return an error (Result or Option), and main itself can return Result<(), E> so ? works at the top level. This turns match-pyramids into a flat, readable happy path.
Applying it: bad user input is the classic recoverable error (12.4), so the guessing game's crashing expect becomes a match that prints a message and continues. Input fails three ways, malformed, out of range, and extraction failure, and each deserves its own message. The judgment of panic versus Result (12.5) comes down to: is the failure expected (the caller can recover, so return Result) or does it mean something is already broken (a bug, so panic)? Validate untrusted data at the boundary, treat contract violations as panics, and lean toward Result in reusable code. unwrap/expect are fine in tests, prototypes, and where Err is genuinely impossible (with expect documenting why).
Finally, error types (12.6) are just types: an enum with a variant per failure kind lets callers match on what went wrong, #[derive(Debug)] gives the programmer's view, and an impl of Display (chapter 16's machinery, shown early) gives the human message. For applications that just need errors to flow, Box<dyn Error> is the catch-all that accepts ? from any error type. Rust replaces C++'s entire exceptions chapter with "errors are values of types you define."
Quiz time
Question #1
Give the one-question test for choosing between panic! and Result.
Show solution
Could a well-written caller recover from this failure? If yes, the failure is expected, so return Result and let them decide. If no, it means an assumption is already broken (a bug), so panic! to stop cleanly. Equivalently: return Result when the caller is better placed than you to respond.
Question #2
For each, what happens?
a)
fn main() {
let n: i32 = "7".parse().unwrap();
println!("{n}");
}
b)
fn parse_two(a: &str, b: &str) -> i32 {
let x = a.parse::<i32>()?;
let y = b.parse::<i32>()?;
x + y
}
c)
fn main() {
let n: i32 = "seven".parse().unwrap();
println!("{n}");
}Show solution
a) Prints 7: "7" parses, unwrap returns the value. b) Refused, E0277: ? is used in a function returning i32, not Result/Option; the fix is to return Result<i32, _> and Ok(x + y). c) Panics: "seven" doesn't parse, and unwrap on the Err panics with called Result::unwrap() on an Err value: ParseIntError ....
Question #3
Rewrite using ? and a main that returns Result:
fn main() {
let a: i32 = match "10".parse() {
Ok(n) => n,
Err(e) => {
println!("error: {e}");
return;
}
};
println!("{}", a * 2);
}Show solution
fn main() -> Result<(), std::num::ParseIntError> {
let a: i32 = "10".parse()?;
println!("{}", a * 2);
Ok(())
}20
main returns Result<(), ParseIntError>, so ? propagates a parse failure straight out (the runtime would print it and exit nonzero), and Ok(()) reports success. Much shorter than the manual match.
Question #4
The capstone. Write a program that totals a list of prices given as strings. Define an error enum PriceError with variants NotANumber(String) (carrying the offending text) and Negative(f64). Write parse_price(text: &str) -> Result<f64, PriceError> that parses an f64 and rejects negatives, and total(prices: &[&str]) -> Result<f64, PriceError> that sums them, propagating the first error with ?. In main, total ["1.50", "2.25", "0.75"] (should succeed) and ["1.50", "oops", "3.00"] (should fail), printing each result.
Show solution
#[derive(Debug)]
enum PriceError {
NotANumber(String),
Negative(f64),
}
fn parse_price(text: &str) -> Result<f64, PriceError> {
let value: f64 = text
.parse()
.map_err(|_| PriceError::NotANumber(text.to_string()))?;
if value < 0.0 {
return Err(PriceError::Negative(value));
}
Ok(value)
}
fn total(prices: &[&str]) -> Result<f64, PriceError> {
let mut sum = 0.0;
for price in prices {
sum += parse_price(price)?;
}
Ok(sum)
}
fn main() {
println!("{:?}", total(&["1.50", "2.25", "0.75"]));
println!("{:?}", total(&["1.50", "oops", "3.00"]));
}Ok(4.5)
Err(NotANumber("oops"))
Design notes. parse_price returns the chapter's error enum, converting a parse failure into NotANumber (carrying the bad text via map_err, lesson 12.4) and rejecting negatives as Negative. total takes a slice (&[&str], lesson 9.6) and uses ? inside the loop, so the first bad price aborts the sum and propagates its error, which is why "oops" stops the second total before "3.00" is even reached. The success case sums to 4.5. (Prices here use f64 for brevity; a real money program counts integer cents, lesson 6.x.)
If you implemented Display for PriceError too, you could print {} for a clean user message; deriving Debug alone is enough for the {:?} output shown.
You can now build programs that fail gracefully: panic only on real bugs, return Result for everything a caller might recover from, propagate with ?, and model errors as the same structs and enums you already know. Chapter 13 turns from what your code does to how it's organized: modules, visibility, splitting across files, and the pub keyword, everything C++ needs headers, header guards, and the preprocessor for, in one chapter.