I’m slowly becoming more proficient working in the Rust language. I’ve now made my third tool, a command-line task manager called fini, and I’m starting to see the patterns that work well. One of them that I really like is how Rust provides convenient ways to handle errors. I think that other languages could really benefit from a similar deep integration of the Result type.
Result is always either a successful value or an error value. Since it is a type, you can specify type arguments to define what those values might be. For example, if a function could return a number or some kind of error, it might have the signature:
fn get_num() -> Result<usize, Box<dyn std::error::Error>>
This means that the function will return either a number like Some(42) or an error like Err("Failed to get number").
By calling the function, you are forced to decide what to do with the error result, should it occur. There are many options for how to do this.
let num = get_num(); // num is a Result
println!("The number was {}", num); // This will not work! You need to get the number out first.
The simplest one, and the first one most developers probably learn is to call expect("Failure") on the result which will return the inner value on success, but it will cause the program to crash on an error. From what I can tell there’s rarely a good reason to use this pattern. It will not even print a nice error message for the user, instead printing a messy backtrace.
Instead, to provide error handling you can either use match or if let to decide what to do with the error case.
match num {
Some(value) => println!("The number was {}", value),
Err => eprintln!("An error occurred"),
};
// Or
if let Some(value) = num {
println!("The number was {}", value);
} else {
eprintln!("An error occurred");
}
This works but it does have the downside that we tend to get a lot of rightward drift; if you get one value, then use that to get another value, then have some conditional logic, we might go multiple levels of indentation deep very quickly. This makes code difficult to read in any language. Indentation should be kept at a minimum.
match num {
Some(value) => {
match get_calculation(value) {
Some(inner) => {
match get_another(inner, value) {
Some(another) => println!("Got final value {}", another), // Getting deep!
Err => eprintln!("An error occurred"),
}
},
Err => eprintln!("An error occurred"),
}
},
Err => eprintln!("An error occurred"),
};
What I’ve found is that there are lots of operations that can fail within the code of a program and most of the time, we don’t care too much about the error case; we just want to either ignore it, use a default value, or report the error and stop the current task.
In other languages we have the concept of an early return to solve this situation. You can test for the error condition and return, allowing the main level of indentation to remain unchanged. This is not quite so obvious how to do in Rust because even if you were to test for the error state with is_err() and then return, the value remains inside a Result and you still need to get it out somehow.
In Rust, the idiomatic way to deal with this situation is to use either let else, unwrap_or, or the question mark ?.
let else is an assignment with an error handler built in. It allows an easy way to handle the early return case in the same way we might in other languages.
let Some(value) = get_num() else {
eprintln!("An error occurred");
return;
};
println!("The number was {}", value);
// No indentation needed!
unwrap_or_default(), unwrap_or(), or unwrap_or_else() can be used to provide a fallback value in case we don’t care about the error at all and just want to make sure the value exists.
let value = get_num().unwrap_or_default(); // Requires that the value's type implements the Default trait
// Or
let value = get_num().unwrap_or(42);
// Or
let value = get_num().unwrap_or_else(|| 10 + 32);
These are great for many situations, but if you have a lot of operations that can fail and you don’t need any special error handling, or if you want to make the error handling available to your own caller, the question mark is truly the best. When you place a question mark after a Result, it will automatically unwrap the value and early return along with the error if there is one.
let value = get_num()?;
println!("The number was {}", value);
So easy! This does not completely get you off the hook, though. To do this, the function you’re in must return a Result of its own, and eventually the error must be handled by something.
However, there’s another feature in Rust that makes this even better. If you make your main function return a Result, then an error will cause the program will exit gracefully with a 0 exit code and print the error message to the console. So as long as this works for you and you don’t want any special error handling, you can just use question marks all the way down!
fn do_calculation() -> Result<usize, Box<dyn std::error::Error>> {
let value = get_num()?;
Some(value * 2)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let result = do_calculation()?;
println!("The result was {}", result);
Ok(())
}
This is so elegant and convenient that you’ll see tons of functions in Rust use the question mark pattern and it can make development a breeze, even while still providing robust error handling. A brilliant design decision and one I enjoy every day.
Photo by Abdul Aziz on Unsplash

Leave a comment