Lesson 9: Error Handling
Introduction
Errors are inevitable in real programs. Rust takes a unique approach — instead of exceptions, it forces you to deal with errors explicitly at compile time. This makes Rust programs more reliable and predictable. Rust has two categories of errors: recoverable errors (using Result) and unrecoverable errors (using panic!).
Unrecoverable Errors with panic!
When something goes so wrong that your program cannot continue, Rust calls panic!. This immediately stops execution and prints an error message. You’ve already seen this happen automatically — for example, when accessing a vector out of bounds.
fn main() {
// Explicit panic
panic!("Something went terribly wrong!");
}
// This also panics automatically:
fn main() {
let v = vec![1, 2, 3];
println!("{}", v[99]); // index out of bounds -> panic!
}- Use
panic!only for truly unrecoverable situations — bugs, impossible states. - For expected failure cases (bad input, missing file), use
Resultinstead.
Recoverable Errors with Result
Result is an enum with two variants: Ok(value) for success and Err(error) for failure. Functions that can fail return a Result instead of crashing.
use std::fs;
fn main() {
let result = fs::read_to_string("hello.txt");
match result {
Ok(contents) => println!("File contents: {}", contents),
Err(e) => println!("Error reading file: {}", e),
}
}Ok(value)— the operation succeeded; contains the result value.Err(e)— the operation failed; contains the error details.- Use
matchto handle both cases explicitly.
Shortcuts: unwrap and expect
For quick prototyping when you’re confident an operation will succeed, you can use unwrap() or expect() to extract the value — but these will panic! if the result is an Err.
use std::fs;
fn main() {
// unwrap: panics with a generic message on Err
let contents = fs::read_to_string("hello.txt").unwrap();
// expect: panics with YOUR message on Err (preferred over unwrap)
let contents = fs::read_to_string("hello.txt")
.expect("Failed to read hello.txt");
println!("{}", contents);
}- Prefer
expect()overunwrap()— the custom message makes debugging much easier. - Both should only be used when you’re certain the operation won’t fail, or in quick prototypes.
Propagating Errors with the ? Operator
The ? operator is Rust’s most ergonomic way to handle errors. If the result is Ok, it unwraps the value. If it’s Err, it returns the error early from the current function — propagating it to the caller.
use std::fs;
use std::io;
fn read_file() -> Result {
// The ? propagates the error if reading fails
let contents = fs::read_to_string("hello.txt")?;
Ok(contents)
}
fn main() {
match read_file() {
Ok(text) => println!("Got: {}", text),
Err(e) => println!("Failed: {}", e),
}
}- The
?operator can only be used in functions that returnResult(orOption). - It replaces verbose
matchblocks and makes error-handling code much cleaner. - Chain multiple
?calls for multiple fallible operations in sequence.
Custom Error Messages
You can return meaningful errors from your own functions by creating a Result with a descriptive string error.
fn divide(a: f64, b: f64) -> Result {
if b == 0.0 {
Err(String::from("Cannot divide by zero!"))
} else {
Ok(a / b)
}
}
fn main() {
match divide(10.0, 2.0) {
Ok(result) => println!("10 / 2 = {}", result),
Err(e) => println!("Error: {}", e),
}
match divide(5.0, 0.0) {
Ok(result) => println!("Result: {}", result),
Err(e) => println!("Error: {}", e),
}
}Interactive Task 1: Safe Division
Write a function safe_sqrt that returns Err if the input is negative, or Ok with the square root otherwise. Call it with both a positive and a negative number and handle both cases with match.
fn safe_sqrt(n: f64) -> Result {
// Your code here
}
fn main() {
// Test with 25.0 and -4.0
}Interactive Task 2: The ? Operator
Write a function parse_and_double that takes a &str, parses it as an i32 using .parse(), and returns double the value. Use the ? operator to propagate any parse error. Call it with both a valid string ("21") and an invalid one ("abc").
fn parse_and_double(s: &str) -> Result {
// Use ? to propagate the error
// Your code here
}
fn main() {
println!("{:?}", parse_and_double("21"));
println!("{:?}", parse_and_double("abc"));
}Mini-Challenge
Write a function first_word that takes a &str sentence and returns Ok with the first word, or Err with a message if the input is empty. Use .split_whitespace().next() to get the first word.
- Hint:
.next()returns anOption— use.ok_or()to convert it to aResult.
fn first_word(sentence: &str) -> Result {
// Your code here
}
fn main() {
println!("{:?}", first_word("hello world"));
println!("{:?}", first_word(""));
}Recap
- panic! is for unrecoverable errors — bugs or impossible states that should never happen.
- Result is for recoverable errors — use
Ok(value)for success andErr(e)for failure. - unwrap() and expect() extract the value but panic on error — prefer
expect()with a descriptive message. - The ? operator propagates errors up the call stack cleanly — the idiomatic Rust way to handle errors in functions.
- Design your own fallible functions to return
Result— it makes your APIs composable and explicit.
Questions about error handling? Drop a comment!