|
1 | 1 | # Nicer error reporting
|
2 | 2 |
|
| 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 | + |
3 | 190 | <aside class="todo">
|
4 | 191 |
|
5 | 192 | **TODO:** Replace `?` with `.with_context(|_| format!("could not read file {}", args.path))`
|
|
0 commit comments