Level 2 · The Rust Way

Error Handling 🚨

Files go missing. Networks drop. Users type "banana" where a number goes. Rust's philosophy: failure is normal, so put it in the type system, honestly, visibly, impossible to forget.

Two kinds of trouble

Panic: pull the emergency brake 🛑

fn main() {
    let v = vec![1, 2, 3];
    // println!("{}", v[99]);      // this line would panic
    panic!("something impossible happened!");  // or panic on purpose
}

A panic is loud, immediate, and points at the guilty line: a crash with a good alibi. But most trouble isn't a bug; it's Tuesday. For that, Rust has something much better.

Result: failure as a value 📬

You already know Option ("maybe a value"). Result is its sibling: "a value or an explanation of what went wrong."

enum Result<T, E> {
    Ok(T),     // it worked, here's the value
    Err(E),    // it failed, here's the error
}

Functions that can fail return it, and the compiler makes callers face both cases:

fn parse_age(text: &str) -> Result<u32, String> {
    match text.trim().parse::<u32>() {
        Ok(n) if n < 150 => Ok(n),
        Ok(_) => Err(String::from("nobody is that old")),
        Err(_) => Err(format!("'{text}' is not a number")),
    }
}

fn main() {
    for input in ["42", "banana", "9000"] {
        match parse_age(input) {
            Ok(age) => println!("age {age}, welcome!"),
            Err(reason) => println!("rejected: {reason}"),
        }
    }
}

Compare that with exceptions in other languages, where failure silently teleports up the call stack until something (hopefully) catches it. In Rust, a function's signature tells you it can fail, and forgetting to handle it is a compile error, not a production outage.

The shortcuts: unwrap family

let n: u32 = "42".parse().unwrap();        // Ok → 42, Err → PANIC 😬
let n: u32 = "42".parse().expect("bad input"); // same, custom message
let n: u32 = "banana".parse().unwrap_or(0);    // Err → fallback: 0
💡 House rules unwrap() in quick experiments: fine, everyone does it. expect("...") when a failure would be a genuine bug: good; the message helps future-you. unwrap_or(default) when there's a sensible fallback: great. Bare unwrap() on user input in real code: how outages are born.

The star of the show: ?

Real work is a chain of things that might fail: open the file, then read it, then parse it. Matching every step drowns the logic in ceremony. The ? operator is Rust's elegant answer:

use std::fs;

fn read_config() -> Result<String, std::io::Error> {
    let raw = fs::read_to_string("config.txt")?;   // Err? return it upward.
    let clean = raw.trim().to_string();            // Ok? keep going.
    Ok(clean)
}

fn main() {
    match read_config() {
        Ok(cfg) => println!("config: {cfg}"),
        Err(e) => println!("couldn't read config: {e}"),
    }
}

Each ? means: "if this failed, stop here and hand the error to my caller; otherwise unwrap and continue." The happy path reads top to bottom like nothing could go wrong, while every failure is still handled. This is the operator Rustaceans miss most in other languages.

One rule: ? only works inside functions that themselves return a Result (or Option): the error needs somewhere to go. That's why we wrote a helper function instead of using ? directly in plain main.

⚠️ Common stumbles
  • Using ? in a function that returns nothing: the compiler will ask "return the error where?" Wrap the logic in a Result-returning function.
  • Forgetting to wrap the final success in Ok(...).
  • unwrap() on anything a user can influence.
Exercise 1

Fortune teller, honest edition

Write fn lucky_number(text: &str) -> Result<i32, String>: parse the text into an i32 (map the parse error to a friendly message), and if the number is 13, return Err("nope, not doing 13".to_string()). Test with "7", "13" and "crab".

Reveal solution
fn lucky_number(text: &str) -> Result<i32, String> {
    let n: i32 = match text.trim().parse() {
        Ok(n) => n,
        Err(_) => return Err(format!("'{text}' isn't a number")),
    };
    if n == 13 {
        Err(String::from("nope, not doing 13"))
    } else {
        Ok(n)
    }
}

fn main() {
    for t in ["7", "13", "crab"] {
        match lucky_number(t) {
            Ok(n) => println!("your lucky number: {n}"),
            Err(e) => println!("error: {e}"),
        }
    }
}
Exercise 2

Chain with ?

Write fn add_strings(a: &str, b: &str) -> Result<i32, std::num::ParseIntError> that parses both strings and returns their sum, using ? twice and zero match expressions.

Reveal solution
fn add_strings(a: &str, b: &str) -> Result<i32, std::num::ParseIntError> {
    let x: i32 = a.trim().parse()?;
    let y: i32 = b.trim().parse()?;
    Ok(x + y)
}

fn main() {
    println!("{:?}", add_strings("20", "22"));   // Ok(42)
    println!("{:?}", add_strings("20", "eel"));  // Err(...)
}

Two lines of logic, full error safety. That's the ? lifestyle.

Exercise 3

Judgment calls

For each situation, pick your tool (panic!, Result, or unwrap_or(default)) and say why: (a) a user mistypes their age in a form; (b) your game's settings file is missing, and defaults exist; (c) an internal function receives a negative array length, which should be impossible.

Reveal reasoning

(a) Result: user input failing is normal; show a friendly message and re-ask. (b) unwrap_or(default)-style fallback: a missing optional file has an obvious recovery. (c) panic!: that's a bug in your own code; crashing loudly at the source beats corrupting data quietly downstream.

🎓 Level 2 complete! Ownership, borrowing, structs, enums, collections, errors: that's the entire heart of Rust. Prove it with the Rust Way quiz, then on to the power tools. 🚀