Errors
In our early discussion on error flow, we enumerated three choices we have when we encounter an error: 1) halt execution, 2) throw an exception, or 3) return an error signal. Rust doesn't have exceptions. Let's see how it supports the other two choices.
Panic
The panic! macro unconditionally halts execution. We might call it directly or it might be triggered by an unrecoverable error, like passing an out-of-bounds index to a [] operation.
The macro accepts a format string and a series of values just like println!. It writes the expanded text to standard error. If called from the main thread, it then stops the entire process, just like exit in C. If called in a spawned thread, it stops only its own thread.
As we discussed earlier, halting execution is a drastic response that should be avoided in production.
Option
For recoverable errors, we want to return to the caller instead of panicking. We need a way to return either an error signal or a successful value. In Haskell, we wrapped up the return type with Maybe, which gave us the variants Just and Nothing. Rust has a similar Option enum with variants Some and None. It has a definition like this:
enum Option<T> {
Some(T),
None
}
enum Option<T> {
Some(T),
None
}This function attempts to produce the next lowercase letter after a given one, returning None if there is no such letter and Some if there is:
fn next_letter(c: char) -> Option<char> {
if 'a' <= c && c < 'z' {
Some((c as u8 + 1) as char)
} else {
None
}
}
fn next_letter(c: char) -> Option<char> {
if 'a' <= c && c < 'z' {
Some((c as u8 + 1) as char)
} else {
None
}
}Error Handling
Returning an Option forces the caller to reckon with a possible error. The only way to get at the value is to check whether it's a Some or None. A poor way to check is to call the unwrap or expect methods:
current = next_letter(current).unwrap();
current = next_letter(current).expect("there's no next letter");
current = next_letter(current).unwrap();
current = next_letter(current).expect("there's no next letter");If the Option is a Some, these methods return the success value. If None, they panic. Calling these methods defeats the purpose of Option. If we wanted to panic, we wouldn't have returned an Option. The only difference between the methods is that expect shows a custom error message. In November 2025, a call to unwrap took out Cloudfare and much of the internet for several hours.
A better response is to inspect which variant we have with pattern matching. In Rust, we do this with match:
current = match next_letter(current) {
None => '🛑',
Some(c) => c,
};
current = match next_letter(current) {
None => '🛑',
Some(c) => c,
};The match expression can be syntactically heavy. If we can identify a sensible default value, we may use an unwrap_or call instead. This function yields the Some value if there is one and the default if there isn't. It's much slimmer than writing our own match expression:
current = next_letter(current).unwrap_or('🛑');
current = next_letter(current).unwrap_or('🛑');Result
The Result enum is like Haskell's Either type. It has two variants: Ok, which holds the valid data, or Err, which holds an error object. It has a definition like this:
enum Result<T, E> {
Ok(T),
Err(E)
}
enum Result<T, E> {
Ok(T),
Err(E)
}Many of the builtin IO functions return a Result, such as read_to_string. This code checks the returned Result with pattern matching and destructuring:
let text_maybe = fs::read_to_string("bookmarks.txt");
match text_maybe {
Err(e) => panic!("Reading failed: {}", e),
Ok(text) => println!("{}", text),
}
let text_maybe = fs::read_to_string("bookmarks.txt");
match text_maybe {
Err(e) => panic!("Reading failed: {}", e),
Ok(text) => println!("{}", text),
}Result and Option support many of the same methods, including unwrap, expect, and unwrap_or.
Try Operator
When we first start programming in Rust, we may feel that our code is drowning in error handling logic. There are two healthy responses to these feelings. First, we should recognize how many errors we were simply ignoring in other languages. Second, we should use syntactic shortcuts to reduce the noise. In Haskell, we saw how to quietly bubble up an error through a functional pipeline with the <$> and <*> operators. Rust has a similar try operator ? that can be used under the following conditions:
-
We are calling functions that return
OptionorResult. -
We are in a function that returns
OptionorResult.
In other words, we can use ? if we are in a function that is part of a functional pipeline. We place it after a dangerous function call. If the call returns an error, it shortcircuits the execution and propagates the error. Consider this function that attempts to call four dangerous functions:
fn obstacle_course() -> Option<i32> {
let a = stage1()?;
let b = stage2()?;
let c = stage3()?.stage4()?;
Some(a + b + c)
}
fn obstacle_course() -> Option<i32> {
let a = stage1()?;
let b = stage2()?;
let c = stage3()?.stage4()?;
Some(a + b + c)
}There's not much noise here, yet the code has error handling and is safe. If any stage fails, we return the error.