Skip to content

Commit

Permalink
Merge pull request rust-cli#54 from rust-lang-nursery/more-book-errors
Browse files Browse the repository at this point in the history
Tutorial: Errors draft
  • Loading branch information
killercup authored Aug 1, 2018
2 parents 2be6168 + 0886a97 commit 3be510f
Showing 1 changed file with 187 additions and 0 deletions.
187 changes: 187 additions & 0 deletions src/tutorial/errors.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,192 @@
# Nicer error reporting

We all can do nothing but accept the fact that errors will occur.
And 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.

## 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`]).

[`read_to_string`]: https://doc.rust-lang.org/1.27.2/std/fs/fn.read_to_string.html
[`Result`]: https://doc.rust-lang.org/1.27.2/std/result/index.html
[`std::io::Error`]: https://doc.rust-lang.org/1.27.2/std/io/type.Result.html

How do you know which it is?
Since `Result` is an `enum`,
you can use `match` to check which variant it is:

```rust
# fn main() -> Result<(), Box<std::error::Error>> {
let result = std::fs::read_to_string("test.txt");
match result {
Ok(content) => { println!("File content: {}", content); }
Err(error) => { println!("Oh noes: {}", error); }
}
# }
```

<aside>

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

</aside>

## Unwrapping

Now, we were able to access 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 need trick to get around that:

```rust
# fn main() -> Result<(), Box<std::error::Error>> {
let result = std::fs::read_to_string("test.txt");
let content = match result {
Ok(content) => { content },
Err(error) => { panic!("Can't deal with {}, just exit here", error); }
};
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.

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`:

```rust
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`:

```rust
# fn main() -> Result<(), Box<std::error::Error>> {
let result = std::fs::read_to_string("test.txt");
let content = match result {
Ok(content) => { content },
Err(error) => { return Err(error); }
};
println!("file content: {}", content);
# Ok(())
# }
```

This, however changes the return type our function needs.
Indeed, 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.
Here's the _full_ example:

```rust
fn main() -> Result<(), Box<std::error::Error>> {
let result = std::fs::read_to_string("test.txt");
let content = match result {
Ok(content) => { content },
Err(error) => { return Err(error); }
};
println!("file content: {}", content);
Ok(())
}
```

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
"Result is okay, as has no content".

<aside>

**Aside:**
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.

</aside>

## Question Mark

Just like calling `.unwrap()` is a shortcut
for the `match` with `panic!` in the error arm,
we have another shortcut for the `match` that `return`s in the error arm:
`?`.

Thats's right, a question mark.
You can append this operator to a value of type `Result`,
and Rust will internally expand this to something very similar to
the `match` we just wrote.

Give it a try:

```rust
fn main() -> Result<(), Box<dyn std::error::Error>> {
let content = std::fs::read_to_string("test.txt")?;
println!("file content: {}", content);
Ok(())
}
```

Very concise!

<aside>

**Aside:**
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 above seen that `read_to_string` returns a [`std::io::Error`].
This works because `?` actually expands to code to _convert_ 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.

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

</aside>

## Providing Context

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

> 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'd be very hard to tell which file was `NotFound`.

<aside class="todo">

**TODO:** Replace `?` with `.with_context(|_| format!("could not read file {}", args.path))`
Expand Down

0 comments on commit 3be510f

Please sign in to comment.