Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 38 additions & 42 deletions src/tutorial/errors.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
# Nicer error reporting

We all can do nothing but accept the fact that errors will occur.
And in contrast to many other languages,
In contrast to many other languages,
it's very hard not to notice and deal with this reality
when using Rust:
As it doesn't have exceptions,
all possible error states are often encoded in the return types of functions.
when using Rust because it doesn't have exceptions.
All possible error states are often encoded in the return types of functions.

## Results

A function like [`read_to_string`] doesn't return a string.
Instead, it returns a [`Result`]
that contains either
a `String`
or an error of some type
(in this case [`std::io::Error`]).
or an error of some type.
In this case, [`std::io::Error`].

[`read_to_string`]: https://doc.rust-lang.org/1.39.0/std/fs/fn.read_to_string.html
[`Result`]: https://doc.rust-lang.org/1.39.0/std/result/index.html
Expand All @@ -36,7 +35,7 @@ match result {

**Note:**
Not sure what enums are or how they work in Rust?
[Check this chapter of the Rust book](https://doc.rust-lang.org/1.39.0/book/ch06-00-enums.html)
[Check out this chapter of the Rust book](https://doc.rust-lang.org/1.39.0/book/ch06-00-enums.html)
to get up to speed.

</aside>
Expand All @@ -45,9 +44,9 @@ to get up to speed.

Now, we were able to access the content of the file,
but we can't really do anything with it after the `match` block.
For this, we'll need to somehow deal with the error case.
The challenge is that all arms of a `match` block need to return something of the same type.
But there's a neat trick to get around that:
For this, we'll need to deal with the error case.
While it's a challenge that all arms of a `match` block need to return something of the same type,
there's a neat trick to get around that:

```rust,no_run
let result = std::fs::read_to_string("test.txt");
Expand All @@ -58,16 +57,15 @@ let content = match result {
println!("file content: {}", content);
```

We can use the String in `content` after the match block.
If `result` were an error, the String wouldn't exist.
But since the program would exit before it ever reached a point where we use `content`,
it's fine.
We can use the String in `content` after the match block, but
if `result` were an error, the String wouldn't exist.
That's fine because the program would exit before it ever reached a point where we use `content`.

This may seem drastic,
but it's very convenient.
If your program needs to read that file and can't do anything if the file doesn't exist,
exiting is a valid strategy.
There's even a shortcut method on `Result`s, called `unwrap`:
There's even a shortcut method on [`Result`] called `unwrap`:

```rust,no_run
let content = std::fs::read_to_string("test.txt").unwrap();
Expand All @@ -76,7 +74,7 @@ let content = std::fs::read_to_string("test.txt").unwrap();
## No need to panic

Of course, aborting the program is not the only way to deal with errors.
Instead of the `panic!`, we can also easily write `return`:
Instead of using `panic!`, we can just use `return`:

```rust,no_run
# fn main() -> Result<(), Box<dyn std::error::Error>> {
Expand All @@ -89,8 +87,8 @@ let content = match result {
# }
```

This, however changes the return type our function needs.
Indeed, there was something hidden in our examples all this time:
However, this changes the return type in our function.
There was something hidden in our examples all this time:
The function signature this code lives in.
And in this last example with `return`,
it becomes important.
Expand All @@ -111,7 +109,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
Our return type is a `Result`!
This is why we can write `return Err(error);` in the second match arm.
See how there is an `Ok(())` at the bottom?
It's the default return value of the function and means
It's the default return value of the function and means:
"Result is okay, and has no content".

<aside>
Expand All @@ -120,7 +118,7 @@ It's the default return value of the function and means
Why is this not written as `return Ok(());`?
It easily could be – this is totally valid as well.
The last expression of any block in Rust is its return value,
and it is customary to omit needless `return`s.
and it is customary to omit a needless `return`.

</aside>

Expand Down Expand Up @@ -154,15 +152,15 @@ Very concise!
There are a few more things happening here
that are not required to understand to work with this.
For example,
the error type in our `main` function is `Box<dyn std::error::Error>`.
But we've seen above that `read_to_string` returns a [`std::io::Error`].
the error type in our `main` function is `Box<dyn std::error::Error>`,
but we've seen above that `read_to_string` returns a [`std::io::Error`].
This works because `?` expands to code that _converts_ error types.

`Box<dyn std::error::Error>` is also an interesting type.
It's a `Box` that can contain _any_ type
that implements the standard [`Error`][`std::error::Error`] trait.
This means that basically all errors can be put into this box,
so we can use `?` on all of the usual functions that return `Result`s.
This means that all errors can be put into this box,
and we can use `?` on all of the usual functions that return a `Result`.

[`std::error::Error`]: https://doc.rust-lang.org/1.39.0/std/error/trait.Error.html

Expand All @@ -172,45 +170,43 @@ so we can use `?` on all of the usual functions that return `Result`s.

The errors you get when using `?` in your `main` function are okay,
but they are not great.
For example:
When you run `std::fs::read_to_string("test.txt")?`
but the file `test.txt` doesn't exist,
For example,
when you run `std::fs::read_to_string("test.txt")?`
and the file `test.txt` doesn't exist,
you get this output:

```text
Error: Os { code: 2, kind: NotFound, message: "No such file or directory" }
```

In cases where your code doesn't literally contain the file name,
it would be very hard to tell which file was `NotFound`.
In cases where your code doesn't actually contain the file name,
it would be hard to tell which file was `NotFound`.
There are multiple ways to deal with this.

For example, we can create our own error type,
and then use that to build a custom error message:
For one, we can create our own error type
and use that to build a custom error message:

```rust,ignore
{{#include errors-custom.rs}}
```

Now,
running this we'll get our custom error message:
Running this, we'll get our custom error message:

```text
Error: CustomError("Error reading `test.txt`: No such file or directory (os error 2)")
```

Not very pretty,
but we can easily adapt the debug output for our type later on.
but we can adapt the debug output for our type later on.

This pattern is in fact very common.
It has one problem, though:
This pattern is very common.
It has one problem though:
We don't store the original error,
only its string representation.
The often used [`anyhow`] library has a neat solution for that:
similar to our `CustomError` type,
its [`Context`] trait can be used to add a description.
Additionally, it also keeps the original error,
so we get a "chain" of error messages pointing out the root cause.
The popular [`anyhow`] library has a neat solution for that:
Its [`Context`] trait can be used to add a description similar to our `CustomError` type.
Additionally, it keeps the original error,
so we get a "chain" of error messages pointing to the root cause.

[`anyhow`]: https://docs.rs/anyhow
[`Context`]: https://docs.rs/anyhow/1.0/anyhow/trait.Context.html
Expand All @@ -219,7 +215,7 @@ Let's first import the `anyhow` crate by adding
`anyhow = "1.0"` to the `[dependencies]` section
of our `Cargo.toml` file.

The full example will then look like this:
The full example will look like this:

```rust,ignore
{{#include errors-exit.rs}}
Expand Down
Loading