内容简介:Rust’s error handling is precise and curious - and in this article, we are going to take a look at why that is the case. I’ll introduce you toof errors in Rust and then explain someThis article is aimed atHumans are decidedly not perfect. Errors and mistak
Summary
Rust’s error handling is precise and curious - and in this article, we are going to take a look at why that is the case. I’ll introduce you toof errors in Rust and then explain some more advanced concepts of dealing with errors. Finally, I am sharing a few tips of my own that I wish I knew when I first started working with Rust.
This article is aimed at Rust beginners and people who are curious about Rust but are yet to make the jump. It should take you about 15 minutes to read and understand, and you don’t need any specific prior knowledge besides basic programming literacy.
Introduction
Humans are decidedly not perfect. Errors and mistakes have been commonplace throughout history and have, in fact, shaped history. If Napoleon packed boots, us Europeans might still all be speaking French today.
Computers are as fallible as their creators, so today, there are about as many ways to deal with errors as there are programming languages. Let’s take a look at some representative examples:
- C solves the problem of potential errors rather elegantly by having no direct error handling, so the obvious solution is just not to make mistakes. If you do accidentally make a mistake, your program will happily trudge through until it confuses itself enough to segfault . That thankfully never happens, and if it does happen, then it’s your fault, not the language’s, and you obviously should have predicted that.
-
Ruby
throws exceptionsraises errors for the most egregious violations, but you also get an occasionalnil
in the mix. Ruby follows the principle of least surprise , so you will not be surprised in the least to get yet another notification of aNoMethodFound
on anil
object from your alerting tool. -
PHP developers are genuinely spoilt for choice - you can drop errors, exceptions, you may decide to return
null
, your method can return a0
or, if you’re feeling adventurous, even a-1
. Most sane PHP developers have settled on using Java-style exceptions in modern code-bases, only resorting to handling error codes when dealing with obscure functions such as most of the ones in the standard library. Unfortunately, you will not always be dealing with modern code-bases, or, for that matter, with sane PHP developers.
Notice a pattern: the languages listed above treat errors as an accidental byproduct. Error handling is seemingly shoehorned into languages as an afterthought, as if someone on the design committee remembered that mistakes could only happen when most of the language was already designed and then saddled a poor intern with the task to find the best hack.
Rust does it differently - errors are first-class citizens , as equal in importance as any other data type. In fact, errors are data types, and its handling is enforced at compile time. You literally can not compile a Rust program that does not handle all of the errors that might pop up.
Let’s take a look at some common patterns of handling errors in Rust.
Option - what if null
, but sane?
An Option
is Rust’s way of saying that your function might return something, but it also might not - in other words, you’re getting Some
or None
. Let’s take a look at this extremely contrived function that might return an Option.
fn how_many_bananas(banana_count: u8) -> Option<u8> { if banana_count > 0 { Some(banana_count) } else { None } } let some_bananas = how_many_bananas(10); // Some(10) let no_bananas = how_many_bananas(0); // None
This code lets you express the possibility that there is a lack
of something very concisely, and the function on the receiving end must
deal with your Option
. There’s no way to extract the banana_count
from the return value without doing something about the possibility of there being None
bananas, even if that something
is consciously ignoring it.
An Option
is Rust’s answer to null
, but just like null
it tells us only that something is not there - not why
is it not there. For that, you have to read on.
Result - when something goes wrong
A Result
expresses the possibility of something breaking in your function. A lot of Rust functions return various Result
types, so expect to be seeing it a lot. The Result
type will let you know if something went Ok
, or if there was an Err
- and which types to expect out of those two values. Let’s rewrite our banana counter to return a Result
:
fn how_many_bananas(banana_count: u8) -> Result<u8, String> { if banana_count > 0 { Ok(banana_count) } else { Err(String::from("We have no bananas!")) } } let some_bananas = how_many_bananas(10); // => Ok(10) let no_bananas = how_many_bananas(0); // => Err("We have no bananas!")
Think of this pattern as being able to return two
types from your function. Returning an error type also means that it’s now someone else’s problem, which is great if you’re working in a team, but a little bit worse if that someone else is future you. As with an Option
, you must deal with the possibility of the function returning an error before you can proceed with the happy path.
Extracting data from Results
Having a Result is all nice and dandy, but you usually want to extract the value from inside of it and get on with your code. There are several different ways to do this - and it all depends on what you want to do.
Pattern matching errors
A simple way of figuring out what’s going on is to use pattern matching on the result type.
match how_many_bananas(0) { Ok(number) => println!("You have {} bananas.", number), Err(error) => println!("{}", error) }
In this case, we extract the number
from our Ok
Result, or error
from our error result, and then do something
with them. Pattern matching is an easy and obvious way of dealing with errors, but it can get a little bit verbose or messy if you wish to chain multiple method calls or if you are dealing with many functions that can return errors inside one function. For example, this pattern is common:
match do_something() { Ok(result) => do_something_else(result), Err(error) => return Err(error) }
Writing 4 lines just to bubble up an error is not very efficient - we will soon discover how to make this more palatable, but before that, we will look at another common method of error handling.
Unwrapping and expecting
Sometimes you have no patience for tedious pattern matching and just want to get on with it.
The unwrap()
method lets you access the “good” value immediately at a slight cost - if the Result
is an Err
, your program panics and stops executing. This method is excellent when you’re learning or rapidly prototyping something, but it could be pretty disastrous in a more extensive program. unwrap()
disregards Rust’s error handling strategy by daring the compiler to fail if it really
cares that much about the error that was raised.
let some_bananas = how_many_bananas(10).unwrap(); // => 10 let no_bananas = how_many_bananas(0).unwrap(); // => panic!
You can also use expect()
, unwrap()
's older brother. expect()
lets you provide your error message, but it won’t do much more than that.
Another way of dealing with a Result is to provide a default value
using the unwrap_or()
function. unwrap_or
lets you quietly fall back on a known good value in case of an error.
let no_bananas = how_many_bananas(0).unwrap_or(0); // => 0
Providing a default value is occasionally useful, but might be a source of subtle bugs - you will not know that an error happened and that a default was used, which might lead to confusion down the line.
Making your own error types
So far, our functions have returned a String
as the error type. This works for smaller programs, but you don’t want to rely on that for more than simple toy apps. A much more powerful way of dealing with errors is to use Rust’s std::error::Error
trait, which is implemented by all standard library errors and should be implemented by your errors as well.
A trait is what you would call an interface in a language such as Java. There are, of course, differences, but that’s a topic for another time.
A simple way to create custom error is to define them via an enum.
#[derive(Debug)] enum CustomErrors { NoBananas, TooManyBananas }
Notice the #[derive(Debug)]
attribute - this tells the compiler that you want to automatically generate code used to display debug information about your type. Attributes are a compelling feature of Rust - you can read more about them in the Rust reference book
.
To promote the CustomErrors
enum to a full-blown error type, we need to implement the Error
trait. Thankfully, Rust automatically generates most of the required code for us, so the implementation looks something like this:
use std::error::Error; impl Error for CustomErrors {}
However, if we try to compile this, the compiler will complain:
error[E0277]: `CustomErrors` doesn't implement `std::fmt::Display` --> src/main.rs:9:6 | 9 | impl Error for CustomErrors {} | ^^^^^ `CustomErrors` cannot be formatted with the default formatter | = help: the trait `std::fmt::Display` is not implemented for `CustomErrors` = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
The compiler helpfully lets us know that lack an implementation of the std::fmt::Display
trait on our error type. We need to implement this type so that Rust knows how to format our errors for output properly.
Implementing that is relatively easy - all we need to do is provide a fmt
function that takes a reference to the error and a reference to the formatter struct. We can then use a handy write!
macro to display details about our error message. To keep it simple, we will just say that there was an error and then output its name, which we can do thanks to the #[derive(Debug)]
attribute on our errors enum.
use std::fmt; impl fmt::Display for CustomErrors { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "error: {:?}", self) } }
From now on, we can use a Result<u8, CustomErrors>
return type for our functions and return Err(CustomErrors::NoBananas)
. Since CustomErrors
is just an enum, you can add your custom logic to it to determine how it will be displayed.
You can click here to open this code in the Rust playground
and play around with it. Try changing the values or adding your errors (for example, ExactlyFiveBananas
).
Bubble up your errors with ?
When writing idiomatic Rust, you generally do not want to cause any side-effects in your libraries. Even if you’re creating a CLI tool, you want to separate the output logic inside of your main.rs
and offload computations and performing actions to a lib.rs
. This pattern lets you re-use various parts of your CLI tools, as well as test them in isolation - but, more importantly for this article, it means that we don’t want to cause any panics (as much as we can help it) or output anything to stdout/stderr.
So, if we don’t want to deal with possible errors immediately, we need a way to bubble them up so that the caller function - ultimately main()
or something as close to the top level of our program - can deal with them. You can do that with pattern matching, but it gets tedious:
fn thing_doer() -> Result<u8, CustomError> { match do_something() { Ok(value) => match do_something_else(value) { Ok(final_value) => Ok(final_value), Err(error) => return Err(error) }, Err(error) => return Err(error) } } fn do_something() -> Result<u8, CustomError> { /* ... */ } fn do_something_else(value: u8) -> Result<u8, CustomError> { /* ... */ }
Instead of doing this, we can instruct Rust to do the same thing using the ?
operator. The question-mark means “execute this statement, and if it returns an error, return with an error, otherwise unwrap the result.” In practice, it might look something like this:
fn thing_doer() -> Result<u8, CustomError> { value = do_something()?; do_something_else(value)? }
This is much cleaner! This code is functionally equivalent to its previous version, but reads nicer.
Mixing and matching different errors
This works great if you’re calling functions that all have the same error type - unfortunately, that is often not the case. Let’s say that, for some reason, you’re also opening a file in the middle of your thing_doer
- and as we can tell from the documentation
, the File::open
function returns a Result that may contain a std::io::Error
type.
This is not the same as CustomError
. Your first instinct may be to change the return type so that it returns a std::error::Error
, after all, that is the trait that is implemented both by our custom error types and the standard library ones.
However, it turns out it’s not so simple: since those are ultimately different types and the compiler won’t know how much memory to allocate for each one, Rust refuses to build that program.
To alleviate that, you can use the Box type . Using a Box, we allocate the memory required for our errors dynamically on the heap. Our code then looks something like this:
fn cold_banana_count() -> Result<u8, Box<dyn std::error::Error>> { let mut fridge = File::open("fridge.txt")?; /* ... some very important banana counting logic ... */ }
Converting between error types
If you don’t want to box your errors, an alternative approach is to convert between standard error types and your custom error types. I prefer this approach because it seems less leaky - I’m not propagating a random std::io::Error
from the middle of my codebase. Instead, I can decide what does that error mean in the context of my library (maybe the cache is missing? maybe I can even recover from it further up the call stack) and provide more information to the end-user.
We can handle this with the map_err
function on the Result
type. map_err
passes through a successful result or apply a provided closure to a potential error value. This lets you map one error type into another one, and then you can use the ?
operator on that result.:
use std::fs::File; enum CustomErrors { NoFridge } fn cold_banana_count() -> Result<u8, CustomErrors> { let mut fridge = File::open("fridge.txt") .map_err(|_| CustomErrors::NoFridge)?; // don't forget the `?` /* ... */ }
While this works, it is much typing. To cut down on that, you can define a conversion function on your CustomErrors
enum, which takes a specific kind of error and convert it to your custom one. The conversion function is automatically called by the ?
operator.
impl From<std::io::Error> for CustomErrors { fn from(error: std::io::Error) -> CustomErrors { CustomErrors::NoFridge } }
Then, you can simply call File::open
with a ?
operator at the end, and the error converts automatically into your NoFridge
error.
Less typing, more functionality
If you think this is a lot of typing, it’s because it is. Thankfully, the Rust community recognizes this and created crates that help with much of the boilerplate.
One of those crates isSnafu, which works similarly to our conversion example, but cuts down significantly on the amount of busywork necessary. There is a full example in the Snafu user’s guide, but the gist is: you create your error enum as you would, derive Snafu
and add attributes to individual errors to teach Snafu how you want them to be displayed. Then, you can call the context
function on a Result to provide context for the custom error quickly.
Snafu is still not entirely stable - if you are looking for something that’s past version 0, you could try out anyhow . Anyhow is more geared towards applications than libraries - it simplifies dealing with errors significantly, but it won’t provide custom errors. For that, you will have to turn to its sister library thiserror , which you can use easily in tandem with Anyhow.
Thanks so much for reading! I hope I did a good job at explaining the ins and outs of Rust error handling and that you learned something new. I’ll be posting more articles like this soon, so keep an eye out on the blog via the RSS feed.
Cover photo by Tanushree Rao on Unsplash
以上所述就是小编给大家介绍的《Errors in Rust: A Deep Dive》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
现代信息检索
(西班牙) Ricardo Baeza-Yates、(巴西)Berthier Ribeiro-Neto / 机械工业出版社 / 2011-3 / 78.00元
本书不仅详细介绍了信息检索的所有主要概念和技术,以及有关信息检索面的所有新变化,而且其组织使读者既可以对现代信息检索有一个全面的了解,又可以获取现代信息检索所有关键主题的详细知识。本书的主要内容由信息检索领域的代表人物Baeza-Yates和Ribeiro-Neto编写,对于那些希望深入研究关键领域的读者,书中还提供了由其他主要研究人员编写的关于特殊主题的发展现状。 与上一版相比,本版在内容......一起来看看 《现代信息检索》 这本书的介绍吧!