Hi folks, today was my first day back at work after a few days off. I can't say I'm thrilled to be back, after all I had a lovely vacation. But I'm excited to get back to the blog 💪
Those of you who've been here since day 1 may feel like the last few blog posts have simply been going over concepts I'd already covered in more detail. That is in part true: after pivoting to work on the Rust book, I have indeed revisited some concepts. But today's blog is hopefully going to bring some novel ideas to the table 🙌
Yesterday's questions answered
No questions to answer
Today's open questions
No open questions
Care about your mistakes
As developers (and humans, I guess) we all make mistakes, and hopefully as kids our parents made it clear to us that learning from your mistakes is a virtuous trait. Some of the biggest mistakes we make when coding may remain totally unknown to us. Untested code that "runs" without errors may be doing something unpleasant without us knowing, especially when our code doesn't have an obvious human interface (like a UI).
This is often a problem in my profession, data engineering. Input data can have millions of possible results, so exhaustive testing is a no go. But incoherent and improbable outcomes can often still be consumed by downstream systems, so a green pipeline can be very misleading.
No programming language can prevent engineering teams from creating these kinds of very much human problems. They can, however, offer a interface between human and machine in the form or errors or exceptions. As we're talking about Rust, we refer to these as errors.
Rust will make you care (or cry) about errors
Before we delve into Rust's various error-handling APIs, I think it's useful to use the Rust book's understanding of errors:
Some errors are recoverable, some are not. When an error is unrecoverable, your program will panic.
Now you may think, that's very vague. But it underpins something important about error handling in Rust: you must (almost) always be explicit about declaring an error as unrecoverable. Let's take the example from the Rust book:
let greeting_file_result = File::open("hello.txt");
And compare it with Python:
greeting_file = pathlib.Path("hello.txt").open("r")
On first glance, these snippets appear very similar, almost identical. But what happens if hello.txt
doesn't exist? In Python, the code will throw a FileNotFoundError. In Rust, your code will compile and run error-free. This is because Rust returns a Result type. This wraps the equivalent NotFound error, so that you can choose when and where to handle it in your code.
If you actually want to get hold of the data in the file in Rust, you must extract either the error or the value from the Result type. This means your code must explicitly handle an error for it to compile. The ways of doing this vary in verbosity. Let's examine the recommended solution for errors that for our program should be deemed unrecoverable.
Imagine hello.txt
contains information vital to your application and you want to make this obvious in the code itself (Uncle Bob wouldn't approve of a comment, after all). Let's refactor the above code snippets to consider this:
let greeting_file = File::open("hello.txt").expect("Hello file must be available at application startup.");
greeting_file = pathlib.Path("hello.txt")
if not greeting_file.exists():
raise Exception("Hello file must be available at application startup.")
greeting_file = greeting_file.open("r")
# OR
try:
greeting_file = pathlib.Path("hello.txt").open("r")
except FileNotFoundError as e:
raise e
In Rust the .expect
method of a Result type allows you to call the panic!
macro with a custom error message. The resultant code reads very nicely. Compare it to your options in Python. Both require extra lines of code and nesting. Also, if style rules are not defined, you could have a code base full of different error handling approaches.
The point is that Rust here is pushing you towards better practices when coding: think about errors, be explicit, and propagate errors in a readable way.
Reducing Return type boilerplate
The above example demonstrates how to induce a panic. Realistically, we wouldn't want to do this in most cases where a function returns a result type. Most of the time we'll expect there to be a value.
So extracting a value via a match statement can get tedious and long. Consider the refactored example again refactored using a match statement:
let greeting_file_result = File::open("hello.txt")
let greeting_file = match greeting_file_result {
Ok(file) => file,
Err(error) => create_hello_file(),
}
Imagine we wanted to create a file instead of inducing a panic if the file doesn't exist. Image then that the create_hello_file
function also return a Result type. You end up with a pretty ugly nested match statement. Enter ?
operator.
As such a case is so common in Rust code, the language has ?
operator. Its usage looks similar to JavaScript ?
operator. The difference is that ?
catches errors instead of undefined
. Thus if we chained the create_hello_file
function with ?
, it would propagate any errors up to the match statement.
The chain operator provides a way to combine function or method calls such that when, for each call, the expected result is returned, the calling function will return the result. Simultaneously, any errors are automatically returned with the result, assuming that the error type is compatible with the calling function's signature.
Differentiating between errors
You can use enums to create kinds or errors. This means that you can match specific error types in match statements and do different things depending on what the error is. You can also use if statements to do the same thing.
When to return Results
This is pretty simple:
When in doubt, return a result rather than panicking.
This makes your code more reusable, as the calling function must decide what to do with the result.
The exception is when arguments don't mean requirements (ints being positive, for example). Then you should panic.
Top comments (0)