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
- Unrecoverable: a bug. The program is in a state that should be impossible. Rust panics: stops immediately with a message.
- Recoverable: expected trouble. A missing file, bad input.
The function returns a
Resultand the caller decides what to do.
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
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.
- Using
?in a function that returns nothing: the compiler will ask "return the error where?" Wrap the logic in aResult-returning function. - Forgetting to wrap the final success in
Ok(...). unwrap()on anything a user can influence.
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}"),
}
}
}
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.
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.