Error Handling

Course 1 · Ch 7
Error Handling
Result<T, E>, the ? operator, and knowing when a failure deserves a value instead of a crash

Chapter 6's Option<T> handled absence. This chapter handles the other "might not work" case: an operation that can fail with a reason why — and, genuinely more than most mainstream languages, Rust's approach here has real common ground with Go's own explicit error handling.

Result<T, E>

enum Result<T, E> { Ok(T), Err(E), }

Same basic shape as Option, but the failure case carries a value (E) explaining what went wrong, not just an absence.

fn divide(a: f64, b: f64) -> Result<f64, String> { if b == 0.0 { Err(String::from("cannot divide by zero")) } else { Ok(a / b) } } match divide(10.0, 0.0) { Ok(result) => println!("Result: {}", result), Err(message) => println!("Error: {}", message), }

Rust making errors part of the function's return type, rather than an exception, is genuinely the same core philosophy Go's own (value, error) convention already uses — a real similarity, not just a contrast, distinguishing both languages from JavaScript/Python's try/catch exception model.

panic! vs. Recoverable Errors

panic! immediately stops the program — reserved for genuinely unrecoverable situations: a real bug, a broken invariant, something that should structurally never happen. Result is for expected, recoverable failure conditions: a missing file, invalid user input, a failed network call. Rust's convention is clear about which to reach for: Result for anything a caller might reasonably want to handle; panic! only for "this should never happen."

The ? Operator

Go's error handling requires an explicit check after every single call that can fail: if err != nil { return err }. Rust's ? operator does the exact same propagation in one character: if the expression is Err, it returns that Err immediately from the current function; if it's Ok, it unwraps the value and execution continues.

fn calculate(a: f64, b: f64, c: f64) -> Result<f64, String> { let step1 = divide(a, b)?; // propagates Err immediately if this fails let step2 = divide(step1, c)?; Ok(step2) }
GoRust
Error typeA separate error return valueErr(E) variant of Result<T, E>
Propagating an error upif err != nil { return err } — repeated at every call? — one character, same call site

unwrap() and expect()

.unwrap() extracts the Ok/Some value directly — or panics if it's actually Err/None. .expect("message") does the same, with a custom panic message. Both are genuinely useful for quick prototypes or cases that are truly certain to succeed — but reaching for them on an operation that could realistically fail in production turns a recoverable situation into a full program crash.

Go and Rust actually agree here
Of the languages compared throughout this course, Go and Rust share the most genuine common ground on error handling: both make errors an explicit part of a function's signature/return value, forcing the caller to acknowledge them, rather than letting them propagate invisibly as exceptions the way JavaScript, Python, and Java's try/catch model does. Rust's ? is best understood as a more concise expression of the exact same idea Go's repeated if err != nil already embodies — not a fundamentally different philosophy.
.unwrap() panicking in production is a real, common mistake
It's tempting to reach for .unwrap() everywhere during early development, since it's shorter than a full match. The real risk: any call that genuinely can fail in production (parsing user input, a network request, a file read) will crash the entire program the moment it does, instead of being handled gracefully. Reserve bare .unwrap() for prototypes, tests, or situations that are truly structurally impossible to fail — everywhere else, use match, ?, or at minimum .expect("a clear message") so a failure at least explains itself before the crash.

Coding Challenges

Challenge 1

Write a function parse_age(input: &str) -> Result<u8, String> that attempts to parse a string into a u8, returning a clear error message on failure. Call it with both a valid and an invalid input, handling both with match.

📄 View solution
Challenge 2

Rewrite this chapter's calculate function (which chains two divide calls using ?) as if Rust had no ? operator at all — using explicit match statements for each call instead. Compare the length and readability to the ? version.

📄 View solution
Challenge 3

Explain when panic! is the right choice versus Result, using one concrete example of each — and explain what's risky about calling .unwrap() on a Result that comes from parsing user-provided input.

📄 View solution

Chapter 7 Quick Reference

  • Result<T, E> — Ok(T) for success, Err(E) for failure with an explanatory value
  • panic! — for unrecoverable bugs/invariant violations, stops the program immediately
  • Result — for expected, recoverable failures (bad input, a missing file, a failed request)
  • ? — propagates an Err immediately, or unwraps Ok and continues; the concise version of Go's repeated if err != nil
  • .unwrap() / .expect("msg") — extract the value or panic; fine for prototypes/tests, risky on anything that can genuinely fail in production
  • Go and Rust share a real philosophy: errors as explicit return values, not exceptions
  • Next chapter: Collections & Iterators — Vec<T>, HashMap<K,V>, iterator adapters, and ownership implications of iterating