Page MenuHomePhorge

article.mdx
No OneTemporary

Authored By
Unknown
Size
19 KB
Referenced Files
None
Subscribers
None

article.mdx

# Error management in Rust
Rust is a relatively new language that is gaining more and more popularity. Its strength is in having a strict compiler that tries to remove entirely some common development errors you would find in other languages, primarily C and its whole class of memory issues.
One of the interesting things about Rust is the error management. Things are done so that it is (normally) impossible to just ignore an error case and force you to deal with them all. Let’s take this very small C snippet:
```c
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
static char const * const HELLO = "Hello, world!\n";
int main(void) {
int fd = open("foo.bar", O_WRONLY | O_CREAT, 0666);
write(fd, HELLO, strlen(HELLO));
close(fd);
return 0;
}
```
An experimented C developer would be able to tell you that the `open`, `write` and `close` calls can fail, and that you _should_ check them. In the current case, if the program cannot execute any of these calls, it just fails silently. More annoyingly, even when enabling all compiler warning flags, even the very broad `-Weverything` clang provides, there is absolutely no warning on this behavior.
You should, after each of these calls, check if they returned `-1`, and in that case, use `errno` and other functions to determine what went wrong, manually print a diagnostic message and exit the program, cleaning resources as needed. The error information – or at least, the information of the presence of an error, not the error itself is the same type and saved in the same place as a valid return, which makes it painfully easy to ignore the error and use that invalid value in next calls like we do here. Again, compiler does not even emit a warning on this, making it very easy to forget to check an error.
Also, amusingly and to add to the pile of issues, in my first writing I forgot the mode argument to `open`, which created a file with nonsensical permissions. Again, not a warning to be seen.
So, how does Rust handle that?
## The `Result` type
Let’s check what the prototype of Rust’s equivalent to `open` is:
```rust
fn create<P: AsRef<Path>>(path: P) -> Result<File>
```
Rust actually provides, in `std::fs::File`, two functions, `open` and `create`, to easily represent opening in `O_RDONLY` and `O_WRONLY | O_CREAT` modes respectively, which are the most common. It also provides, for more specific cases, the `options` function giving you a builder pattern to set your options safely, but our concern here is the result type. For our example, the equivalent is `create`.
The return type of `create` is `Result<File>`. Actually, Rust takes some shortcuts in its standard library, and in this case, uses an alias to remove an often repeated argument. The actual type is `std::io::Result<File>` which expands to `Result<File, std::io::Error>`. It is an implementation detail, but we need the expanded type here to understand.
A result is an `enum`, which can takes a closed amount of values. Unlike C enums, which are just numerical values with labels, Rust enum variants can be simple tagged values as well as more complex structures. Take a closer look at the `Result` type definition:
```rust
enum Result<T, E> {
Ok(T),
Err(E),
}
```
The enum Result can take only two values: `Ok`, or `Err`. Each of these variants are a 1-item tuple, containing an item of type `T` or `E` from the generic typing.
You cannot access the `T` value directly, however: you have in some way or another eliminate the possibility of the result being `Err`. The most straightforward way would be with `match`:
```rust
match File::open("foo.bar") {
Ok(f) => { write!(f, "Hello, world!\n"); }
Err(e) => { println!("Error opening file: {}", e); }
}
```
You cannot work on the file if an error happens, as you will not be able to get a `File` value from `Err`, needed to do write operations. You have to _at least_ throw the error away, which is then an active choice.
If you put this code in a Rust file and compile it, you will actually get a warning:
```
warning: unused `Result` that must be used
--> src/main.rs:7:13
|
7 | write!(f, "Hello, world!\n");
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: this `Result` may be an `Err` variant, which should be handled
= note: `#[warn(unused_must_use)]` on by default
```
The call to `write!` is also returning a `Result`, and by default, the compiler will warn you if you call something that returns a `Result` and you ignore it. This way, it is way harder to accidentally ignore an error case.
Now, you can add another `match` block around `write!` to check for error at this point, but doing so gets heavy quickly code-wise. Thankfully, Rust provides tools to ease all of that.
## Managing errors
`Result` provides some useful functions to deal with errors without having to `match` all the time. The first and simplest one is `unwrap`. Calling it on a `Result` containing an `Err` will cause a `panic!`, crashing the program instantly, and otherwise just returning the `T` value. So, using it, we can reduce our code to this:
```rust
let mut f = File::open("foo.bar").unwrap();
write!(f, "Hello, world!\n").unwrap();
```
The code is still rather small, and unlike the C sample above, an error will actually close the program instantly with both an error code and a message printed explaining what happened:
```
thread 'main' panicked at src/main.rs:5:42:
called `Result::unwrap()` on an `Err` value: Os { code: 13, kind: PermissionDenied, message: "Permission denied" }
```
However, while very practical, `unwrap` is not really known as best practice. It can be useful for small tests or temporary handling, but you really should not rely on it for long-lasting code. Or put it for cases where you are **absolutely sure** that you cannot have an error.
First of all, it does not give a lot of information. You do have the source file and line, but it can still make it hard to get good information on what happened. Thankfully, `unwrap` has a little brother, `expect`, which allows to make the error a bit more informative by adjoining a custom message:
```rust
let mut f = File::create("foo.bar").expect("Cannot open file");
write!(f, "Hello, world!\n").expect("Cannot write to file");
```
If you run this code, you will see that the message in `expect` is prefixed to the lower error code. In big code bases, this additional context can save a lot of debugging time, and it is barely longer to write.
It is advised to always favor `expect` to `unwrap` so you have a bit more context in case of failure.
What if, however, you do not want to treat an error as an exit? Take for example reading an environment variable: if it does not exist, I want to continue my program anyway. Well, `match` still works:
```rust
let var = match std::env::var("MY_VAR") {
Ok(v) => v,
Err(_) => "foo".to_string(),
};
```
You can see we completely ignore the error in this case. However, there are shorter ways! `Result` provides `unwrap_or` and some other functions for these use cases.
```rust
let var = std::env::var("MY_VAR").unwrap_or("foo".to_string());
```
Note that in this case, the value given to `unwrap_or` is always calculated to be passed to the function, which is OK for trivial values, but not for expensive to calculate functions or stuff that has side effects. In that case, you have `unwrap_or_else` which takes a lambda, executed only if the `Err` case is encountered:
```rust
let var = std::env::var("MY_VAR").unwrap_or_else(|| "foo".to_string());
```
In this last example, the `"foo"` string copy is allocated only if the environment variable is not found, while on the previous one, it was always allocated, then wasted if you had a `Ok`.
Also, if your `T` type implements `Default`, you can just use `unwrap_or_default`. If an empty string is OK for you:
```rust
let var = std::env::var("MY_VAR").unwrap_or_default();
```
And that is only scratching the surface.
- `map` and `map_err` allow you to call a mapping function in `Ok` and `Err` cases respectively, allowing you to change the types of your `Result` without deconstructing it.
- `or` and `and` allow you to combine multiple results into a single one based on their respective values. Their counterpart `or_else` and `and_then` do the same but with a lambda called in the appropriate case. It allows transformation like `map` and `map_err` but fallible.
- `is_ok` and `is_err` are simple methods to check if you have a `Ok` or `Err` without retrieving their inner values. Their siblings, `is_ok_and` and `is_err_and`, allows to do the same and perform an additional check on the contained value while not restructuring the `Result`.
And some more that I did not describe for some more use cases you could have. You can check [`Result`’s documentation](https://doc.rust-lang.org/std/result/enum.Result.html) to see all the available methods and a complete description for each.
## Going back up
What if I told you that Rust makes it easy to make errors bubble up your stack, and making writing your program to exit gracefully from its `main` even in deep error cases much easier?
Let us take our example back, and put it in a separate function returning a `Result`:
```rust
use std::fs::File;
use std::io::Write;
fn say_hello() -> Result<(), std::io::Error> {
let mut f = match File::create("foo.bar") {
Ok(f) => f,
Err(e) => return Err(e),
};
match write!(f, "Hello, world!\n") {
Ok(_) => Ok(()),
Err(e) => return Err(e),
}
}
fn main() {
say_hello().unwrap();
}
```
Too verbose. Thankfully, we can do shorter. First, the second `match` is completely useless, we can just return the `Result` from `write!` directly. Easy.
The `File::create` part however is a bit more tricky, as we need to do an early return. Well, Rust provides an operator for that: `?`. It is basically the `match` seen above, but compacted into a single character. So our code above now reads:
```rust
fn say_hello() -> Result<(), std::io::Error> {
let mut f = File::create("foo.bar")?;
write!(f, "Hello, world!\n")
}
```
Much shorter, but still handing every error case and passing it up in the stack. Also, like in C++ for example, objects with destructors (`Drop` trait) will be destroyed with the early `return`, clearing resources on the way.
The `unwrap` in `main` handles all errors coming from the `say_hello` function in a single place. But that still triggers a `panic!`. You can also `match` at this level to print the error cleanly. But Rust has another small trick up its sleeve to ease that too: you are perfectly allowed to make `main` return a `Result`, and it will automatically take care of printing the error and exiting with an error status code for you.
```rust
fn main() -> Result<(), std::io::Error> {
say_hello()
}
```
Voilà, you have complete error management in your program without having to write much. So while Rust prevents you from just throwing errors away, it also makes it easy to handle them properly.
However, there are rare cases where you do not actually care about a result value, neither its success nor error value. If you just do your call, the compiler will complain that you have an unused `Result` (rightfully), and once again, you have to actively tell it you do not actually want to use the value. There are two ways of doing that: either you assign it to a `_` variable, dropping it instantly, or you use `ok` to transform it to an `Option` which will clear the warning.
```rust
let _ = say_hello();
say_hello().ok();
```
I prefer the second solution personally as it makes it more visible you are “OK’ing” an error.
## Multiple error types
You have used another function which has a different error type, and you do not know how to manage it. For example:
```rust
fn main() -> Result<(), std::io::Error> {
let n = std::env::var("FOOBAR")?;
let mut f = File::open(n)?;
write!(f, "foobar")
}
```
While `std::env::var` does return a `Result`, its error part is `std::env::VarError`, which is not compatible with `std::io::Error`. The compiler will argue that it cannot do the conversion from one to another. We need to manage this.
Thankfully, error types are (normally) implementing the `std::error::Error` trait. We can use that to have a generic error return, using a `Box`.
Let us modify the code a bit:
```rust
fn main() -> Result<(), Box<dyn std::error::Error + 'static>> {
let n = std::env::var("FOOBAR")?;
let mut f = File::open(n)?;
write!(f, "foobar")
}
```
Note that this still does not compile, but we will get back to this later. The error moved to the `write!` call, meaning it solved the issue for the `?` calls.
You can notice that you do not have to do any conversion using the `?`. The `?` operator will automatically call `.into()` on the error value before returning it, and there is a `From` conversion to automatically convert any implementor of `Error` into a `Box<dyn Error>` for convenience. So as long as you have a compatible type, this will work automagically.
For the remaining error, though, we do not have this automatic conversion. Amusingly, the compiler proposes the following solution:
```rust
Ok(write!(f, "foobar")?)
```
It certainly works: if there is an error, it does the early return automatically, otherwise it extracts the `Ok` value and puts it in a new `Ok` with the right type. Certainly works but, in my option, a bit silly. You can just make the `into` call by hand:
```rust
write!(f, "foobar").map_err(Into::into)
```
It is a bit longer to write though, but personally I find it clearer to have `into` written rather than a wild `Ok` which does not hint at the error conversion happening.
Note however that while using `dyn Error` is very practical, it can be more adapted to make your own error type and provide conversions from the “lower” types into yours that you use everywhere, especially for libraries. We will see how to do that in a few paragraphs.
## Making your own errors
Sometimes, you want to emit errors on your own. Rust also makes you able to do that, with varying degrees of complexity depending on your needs.
The most simple is to use basic strings. Rust provides ways of turning strings into full-fledged errors for your convenience.
```rust
fn main() -> Result<(), Box<dyn std::error::Error + 'static>> {
Err("Something's wrong".into())
}
```
Once again, a `From` implementation makes the conversion using `into()` trivial. Actually, there are `From` implementation for `&str`, `String` and even `Cow`, so you are covered in all string cases.
Since our type supports `into()`, it means it also works with `?`:
```rust
fn main() -> Result<(), Box<dyn std::error::Error + 'static>> {
let v: Result<(), &'static str> = Err("Aieee");
println!("{:?}", v?);
Ok(())
}
```
You can see the different `Result` type (on the `E` side) between `v` and the return of `main`: `?` does the conversion automatically.
Since you can then use `Result<T, &str>` transparently, it also means that it is easy to make `Option::ok_or` or `Result::map_err` pretty easily:
```rust
fn main() -> Result<(), Box<dyn std::error::Error + 'static>> {
let v: Option<()> = None;
v.ok_or("Ouch")?;
Ok(())
}
```
### Implementing `Error`
Using strings as error is good for quick programming, but if you want to be more robust, you will prefer to make your own, full-fledged error type. To do that, you need to implement the `Error` type from the standard library. This is a very basic one to start with:
```rust
use std::fmt;
struct Failure;
impl fmt::Display for Failure {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Something's wrong")
}
}
impl fmt::Debug for Failure {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Something's wrong")
}
}
impl std::error::Error for Failure {}
```
You can use the `Failure` value as an error anywhere, even with the automatic conversion to `Box<dyn Error>` if needed, or use it as a `E` for the return value of `main`. It all works seamlessly.
`Error` requires you to implement both `Display` and `Debug` so your error can be inspected: you can use them to display basic or complex information at your will. For reference, the value printed by `main`’s return comes from `Debug`.
You can of course put values in your structure, and use them for the error messages to give more information. You can also use an enum.
### Wrapping errors
As you make your own error type, you may want to use it as a wrapper for other types of error that can happen. Let’s broaden our type so it can also represent errors of `std::io::Error` type that are common in the standard library.
We can write a `From` implementation to make the conversion to our type possible:
```rust
impl From<std::io::Error> for Failure {
fn from(_: std::io::Error) -> Failure {
Failure
}
}
```
And voilà, we can now use `?` to convert from `std::io::Error` to `Failure` automatically and use our own type everywhere. However, we are wasting precious information that we could keep. Let us modify our type so it can represent both our own emitted errors and the I/O errors while keeping information, leveraging an enum.
```rust
enum Failure {
Own,
Io(std::io::Error),
}
impl From<std::io::Error> for Failure {
fn from(inner: std::io::Error) -> Failure {
Failure::Io(inner)
}
}
```
However, we are still not using this information (the compiler will gently inform you that you never use your field), and the error message on runtime is not any more informative. Naturally, we can just modify our printing functions to do so:
```rust
impl fmt::Debug for Failure {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Failure::Own => write!(f, "Something's wrong"),
Failure::Io(e) => write!(f, "IO Error: {:?}", e),
}
}
}
```
If you try it now, you will see that you can see the inner error. But there is a better way to do so. Rust planned that errors can bubble up and generalize along the way, and provides a mechanism specifically to represent that. In the `Error` trait, there is an optional method to implement, `source`, made explicitly to give a source error to the current one, cascading down. Let us implement that:
```rust
impl std::error::Error for Failure {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Failure::Own => None,
Failure::Io(inner) => Some(inner),
}
}
}
```
Note however that Rust’s implementation of the `main` return does not use this information (it only relies on `Debug` implementation), but some other libraries managing errors do take it into account. You can also use it if you write your own generic error handler.
## Wrapping up
We have seen some important points about error handling in Rust:
- It is conceived to make it impossible to accidentally ignore an error case. The `Result` type, one of the most important in the language, ensures that you deal with the error appropriately, or at least make by yourself the active choice of ignoring an error instead of silently dropping it. Furthermore, it gives a unified interface for error management, with different types for the return of a success and an error, removing the need for special values like `-1`, things like `errno` or a writable pointer passed to a function to get an error.
- While Rust does not have throwable exceptions, it provides an easy way to bubble errors up your stack and get them to elegantly be managed at the right place. The `?` operator is a cornerstone of Rust code, allowing efficient and non-verbose management of all error sources.
- It is trivial to create your own error types that integrate into the Rust way of doing things, making managing your custom errors and adding context or generalization while bubbling up trivial.
All of this is one of the most basic foundations of Rust and what makes code written in it more robust. Even if you willingly ignore an error with `unwrap`, it will not silently fail with weird side effects but actually crash your program, and finding uses of these methods across a codebase is easy, compared to having to check that every call to `open` is followed by a `if (fd == -1)`.

File Metadata

Mime Type
text/x-c
Expires
Sun, Oct 25, 16:43 (1 d, 16 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
108
Default Alt Text
article.mdx (19 KB)

Event Timeline