Skip to content

Commit 3be510f

Browse files
authored
Merge pull request rust-cli#54 from rust-lang-nursery/more-book-errors
Tutorial: Errors draft
2 parents 2be6168 + 0886a97 commit 3be510f

File tree

1 file changed

+187
-0
lines changed

1 file changed

+187
-0
lines changed

src/tutorial/errors.md

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,192 @@
11
# Nicer error reporting
22

3+
We all can do nothing but accept the fact that errors will occur.
4+
And in contrast to many other languages,
5+
it's very hard not to notice and deal with this reality
6+
when using Rust:
7+
As it doesn't have exceptions,
8+
all possible error states are often encoded in the return types of functions.
9+
10+
## Results
11+
12+
A function like [`read_to_string`] doesn't return a string.
13+
Instead, it returns a [`Result`]
14+
that contains either
15+
a `String`
16+
or an error of some type
17+
(in this case [`std::io::Error`]).
18+
19+
[`read_to_string`]: https://doc.rust-lang.org/1.27.2/std/fs/fn.read_to_string.html
20+
[`Result`]: https://doc.rust-lang.org/1.27.2/std/result/index.html
21+
[`std::io::Error`]: https://doc.rust-lang.org/1.27.2/std/io/type.Result.html
22+
23+
How do you know which it is?
24+
Since `Result` is an `enum`,
25+
you can use `match` to check which variant it is:
26+
27+
```rust
28+
# fn main() -> Result<(), Box<std::error::Error>> {
29+
let result = std::fs::read_to_string("test.txt");
30+
match result {
31+
Ok(content) => { println!("File content: {}", content); }
32+
Err(error) => { println!("Oh noes: {}", error); }
33+
}
34+
# }
35+
```
36+
37+
<aside>
38+
39+
**Aside:**
40+
Not sure what enums are or how they work in Rust?
41+
[Check this chapter of the Rust book](https://doc.rust-lang.org/1.27.2/book/second-edition/ch06-00-enums.html)
42+
to get up to speed.
43+
44+
</aside>
45+
46+
## Unwrapping
47+
48+
Now, we were able to access content of the file,
49+
but we can't really do anything with it after the `match` block.
50+
For this, we'll need to somehow deal with the error case.
51+
The challenge is that all arms of a `match` block need to return something of the same type.
52+
But there's a need trick to get around that:
53+
54+
```rust
55+
# fn main() -> Result<(), Box<std::error::Error>> {
56+
let result = std::fs::read_to_string("test.txt");
57+
let content = match result {
58+
Ok(content) => { content },
59+
Err(error) => { panic!("Can't deal with {}, just exit here", error); }
60+
};
61+
println!("file content: {}", content);
62+
# }
63+
```
64+
65+
We can use the String in `content` after the match block.
66+
If `result` were an error, the String wouldn't exist.
67+
But since the program would exit before it ever reached a point where we use `content`,
68+
it's fine.
69+
70+
This may seem drastic,
71+
but it's very convenient.
72+
If your program needs to read that file and can't do anything if the file doesn't exist,
73+
exiting is a valid strategy.
74+
There's even a shortcut method on `Result`s, called `unwrap`:
75+
76+
```rust
77+
let content = std::fs::read_to_string("test.txt").unwrap();
78+
```
79+
80+
## No need to panic
81+
82+
Of course, aborting the program is not the only way to deal with errors.
83+
Instead of the `panic!`, we can also easily write `return`:
84+
85+
```rust
86+
# fn main() -> Result<(), Box<std::error::Error>> {
87+
let result = std::fs::read_to_string("test.txt");
88+
let content = match result {
89+
Ok(content) => { content },
90+
Err(error) => { return Err(error); }
91+
};
92+
println!("file content: {}", content);
93+
# Ok(())
94+
# }
95+
```
96+
97+
This, however changes the return type our function needs.
98+
Indeed, there was something hidden in our examples all this time:
99+
The function signature this code lives in.
100+
And in this last example with `return`,
101+
it becomes important.
102+
Here's the _full_ example:
103+
104+
```rust
105+
fn main() -> Result<(), Box<std::error::Error>> {
106+
let result = std::fs::read_to_string("test.txt");
107+
let content = match result {
108+
Ok(content) => { content },
109+
Err(error) => { return Err(error); }
110+
};
111+
println!("file content: {}", content);
112+
Ok(())
113+
}
114+
```
115+
116+
Our return type is a `Result`!
117+
This is why we can write `return Err(error);` in the second match arm.
118+
See how there is an `Ok(())` at the bottom?
119+
It's the default return value of the function and means
120+
"Result is okay, as has no content".
121+
122+
<aside>
123+
124+
**Aside:**
125+
Why is this not written as `return Ok(());`?
126+
It easily could be -- this is totally valid as well.
127+
The last expression of any block in Rust is its return value,
128+
and it is customary to omit needless `return`s.
129+
130+
</aside>
131+
132+
## Question Mark
133+
134+
Just like calling `.unwrap()` is a shortcut
135+
for the `match` with `panic!` in the error arm,
136+
we have another shortcut for the `match` that `return`s in the error arm:
137+
`?`.
138+
139+
Thats's right, a question mark.
140+
You can append this operator to a value of type `Result`,
141+
and Rust will internally expand this to something very similar to
142+
the `match` we just wrote.
143+
144+
Give it a try:
145+
146+
```rust
147+
fn main() -> Result<(), Box<dyn std::error::Error>> {
148+
let content = std::fs::read_to_string("test.txt")?;
149+
println!("file content: {}", content);
150+
Ok(())
151+
}
152+
```
153+
154+
Very concise!
155+
156+
<aside>
157+
158+
**Aside:**
159+
There are a few more things happening here,
160+
that are not required to understand to work with this.
161+
For example,
162+
the error type in our `main` function is `Box<dyn std::error::Error>`.
163+
But we've above seen that `read_to_string` returns a [`std::io::Error`].
164+
This works because `?` actually expands to code to _convert_ error types.
165+
166+
`Box<dyn std::error::Error>` is also an interesting type.
167+
It's a `Box` that can contain _any_ type
168+
that implements the standard [Error][`std::error::Error`] trait.
169+
This means that basically all errors can be put into this box,
170+
so we can use `?` on all of the usual functions that return `Result`s.
171+
172+
[`std::error::Error`]: https://doc.rust-lang.org/1.27.2/std/error/trait.Error.html
173+
174+
</aside>
175+
176+
## Providing Context
177+
178+
The errors you get when using `?` in your `main` function are okay,
179+
but great they are not.
180+
For example:
181+
When you run `std::fs::read_to_string("test.txt")?`
182+
but the file `test.txt` doesn't exist,
183+
you get this output:
184+
185+
> Error: Os { code: 2, kind: NotFound, message: "No such file or directory" }
186+
187+
In cases where your code doesn't literally contain the file name,
188+
it'd be very hard to tell which file was `NotFound`.
189+
3190
<aside class="todo">
4191

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

0 commit comments

Comments
 (0)