Good evening everyone, I'm back after a few days off. I had a friend from abroad visiting and put my studies to one side. I'm back now and have three Rust blogs planned for this week. Today's blog will be short and touch on error handling concepts rather than novel features
Yesterday's questions answered
No questions to answer
Today's open questions
No open questions
Handling errors like a pro
We've already looked at how errors are handled in Rust. But today while building a simple CLI tool (a chapter in the Rust book), I learned some new techniques for error handling could be applied to Rust an beyond.
Keep things simple
When reading code you have a number of markers of cognitive complexity (in terms of syntax, not the concept being represented in code):
- Vertical complexity: how deep am I in the stack when this code runs?
- Horizontal complexity: how many related data points do I need to track to understand how a function works?
- Durational complexity: how long do I need to follow the code before I can "drop" variables ?
When it comes to error handling, we often ask ourselves: where do I raise an error? This indicates to me that reducing vertical and durational complexity can help us handle errors better. In practice, this means two things:
- Errors should be handled at a single, higher level. Errors represent potential exit points. Therefore grouping these exit points at one point in call stack can make your code more expressive.
- Handle errors as early as possible. If we can rule out all errors early on, then whatever remaining code can be simplified as the "happy path".
Nuanced error handling syntax
Rust executables often contain a run
function. This contains the application logic and often serves the purpose of bubbling errors up to your main file, as well as making your code testable. As a run
function typically returns an empty tuple
, often don't want to do anything with the variable.
The idiomatic of handling such errors in Rust is to use an if let
statement:
if let Err(e) = minigrep::run(config) {
eprintln!("Application error: {e}");
process::exit(1);
};
We don't care about the empty tuple returned by the run function, so we can save some noise in the catch all variable.
However, it's more often the case that we do, indeed, care about what our function returns to use. In that instance, we'd want to use the unwrap_or_else
method of the Result
type:
let config = Config::build(&args).unwrap_or_else(|err| {
eprintln!("Problem parsing arguments: {err}");
process::exit(1)
});
Top comments (0)