diff --git a/src/tutorial/errors.md b/src/tutorial/errors.md index 5473294..2420a56 100644 --- a/src/tutorial/errors.md +++ b/src/tutorial/errors.md @@ -1,11 +1,10 @@ # 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 @@ -13,8 +12,8 @@ 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 @@ -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. @@ -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"); @@ -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(); @@ -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> { @@ -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. @@ -111,7 +109,7 @@ fn main() -> Result<(), Box> { 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". @@ -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`. -But we've seen above that `read_to_string` returns a [`std::io::Error`]. +the error type in our `main` function is `Box`, +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` 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 @@ -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 @@ -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}}