Result is how Rust lets a function return either a useful value or a useful error. The part I had to get comfortable with was that receiving a Result means I still have a decision to make.
What should I do with a Result<T, E>?
The value can contain success or failure. The caller decides what happens next.
Reveal the answer
Match Ok and Err, propagate the error with ?, or use unwrap only when a panic is acceptable.
What a Result contains
The type has two variants:
enum Result<T, E> {
Ok(T),
Err(E),
}Ok(T) carries the success value. Err(E) carries the error value. Both types are generic, so a function chooses what success and failure mean for its own job.
Here is a deliberately small example:
fn is_even(number: u32) -> Result<String, String> {
if number % 2 == 0 {
Ok(format!("{number} is even!"))
} else {
Err(format!("{number} is not even!"))
}
}For this function, both variants contain a String. The variant itself tells me whether the operation succeeded.
Why unwrap can panic
This is the shortest way to get the success value:
fn main() {
let message = is_even(2).unwrap();
println!("{message}");
}2 is even!
But unwrap does not handle the error case. It panics if the value is Err:
fn main() {
let message = is_even(1).unwrap();
println!("{message}");
}thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value:
"1 is not even!"'
The tradeoff with unwrap
unwrap is convenient when a failure really should stop the program, or while I am writing a quick experiment. It is a poor substitute for deciding how a real error should be handled.
Propagate the error with question mark
The ? operator extracts the value from Ok. If it sees Err, it returns that error from the current function:
fn print_even(number: u32) -> Result<(), String> {
let message = is_even(number)?;
println!("{message}");
Ok(())
}
fn main() -> Result<(), String> {
print_even(2)?;
Ok(())
}The function using ? must return a compatible error type. Here both print_even and main return Result<_, String>, so the error can move up the call stack unchanged.
Handle both variants with match
When I want different behavior for success and failure, match makes both paths explicit:
fn main() {
match is_even(1) {
Ok(message) => println!("{message}"),
Err(error) => println!("Could not continue: {error}"),
}
}Could not continue: 1 is not even!
No panic, and no error path hidden from the reader. This is more typing than unwrap, but it gives me full control.
What I keep in my head
Use match when both paths need behavior, ? when the caller should handle the error, and unwrap only when stopping immediately is the intended behavior.