I'll hype my own library, SNAFU [1].<p>It simplifies constructing your own "leaf" errors and streamlines the ability of collecting multiple types of errors while attaching more context to them (e.g. filenames, stack traces, user ids, etc.). It allows you to smoothly switch from "stringly-typed" errors to strongly-typed errors. You can create opaque errors to avoid leaking internal implementation details into your public API.<p>Applied to the code in the post:<p><pre><code> use snafu::prelude::*;
use std::{fs::File, io::prelude::*};
#[derive(Debug, Snafu)]
enum Error {
#[snafu(display("Unable to open {filename}"))]
Opening {
source: std::io::Error,
filename: String,
},
#[snafu(display("Unable to read {filename}"))]
Reading {
source: std::io::Error,
filename: String,
},
#[snafu(display("Unable to parse {buffer} as a number"))]
Parsing {
source: std::num::ParseIntError,
buffer: String,
},
}
fn read_number_from_file(filename: &str) -> Result<u64, Error> {
let mut file = File::open(filename).context(OpeningSnafu { filename })?;
let mut buffer = String::new();
file.read_to_string(&mut buffer)
.context(ReadingSnafu { filename })?;
let buffer = buffer.trim();
let parsed: u64 = buffer.parse().context(ParsingSnafu { buffer })?;
Ok(parsed)
}
</code></pre>
The key parts are the `derive(Snafu)` on the definition of the error enum and the usages of `.context` and `XxxSnafu` at the error sites.<p>Importantly, this example demonstrates a key feature of SNAFU, here shown as "not all `io::Error`s are the same". Opening the file and reading the file are two separate error conditions and should <i>not</i> be lumped together as one.<p>[1]: <a href="https://docs.rs/snafu/0.7.0-beta.1/snafu/index.html" rel="nofollow">https://docs.rs/snafu/0.7.0-beta.1/snafu/index.html</a>
The fact that failure conditions from both `File::open` and `read_to_string` become an `IoError` is a significant roadblock to making these errors useful. The mechanism as described in this blog post also fails to introduce contextual information about the reason a failure has occurred.<p>This means that errors, if implemented as described in this post, either formatted or handled don't give sufficient information to the caller/user on how to deal with the error.<p>EDIT:<p>I strongly recommend to use `op.map_err(|e| SomeError::Open(e, filename))?` and `op.map_err(SomeError::Read)?` as an alternative when propagating the errors. It is more typing at the location of propagation than just `?`, but the errors this approach produces are immediately actionable regardless of whether they are printed to the user or handled by a caller. Provided, of course, this pattern is applied consistently.
Propagation without needing to use boxed trait objects can also be accomplished using `anyhow`[1]/`eyre`[2], which have nice downcasting API’s for recovering the original error type if you know what the possibilites could be. I only bring it up because they aren’t mentioned until the end of the article and only in passing but they offer really nice features for attaching context and downcasting that makes up for the pseudo-type-erasure.<p>1: <a href="https://github.com/dtolnay/anyhow" rel="nofollow">https://github.com/dtolnay/anyhow</a><p>2: <a href="https://github.com/yaahc/eyre" rel="nofollow">https://github.com/yaahc/eyre</a>
I find this kind of article that explains how to do something with vanilla Rust valuable because many articles will instead explain how to use a crate that offers similar functionality. This is the only way to know if a crate can actually pull its weight before you decide to adopt it.
For reducing the code to implement this pattern, `thiserror` is nice. `derive_more` also has similar functionality, and I tend to use that more personally because I use its other derives, and while thiserror has a slightly nicer API (derive_more will assume a unit struct's field is it's source, which is wrong more often than not in my codebase, since I avoid overdoing nested error enums), it's not worth adding another dep to do what the dep I already have can do: <a href="https://jeltef.github.io/derive_more/derive_more/error.html" rel="nofollow">https://jeltef.github.io/derive_more/derive_more/error.html</a>
Error handling in Rust is one of the reasons I went back to Go. It's a lot of mental overhead having to constantly think about all the different types of errors from different crates that could be returned and how to handle them at the caller. I don't want to have to import somebody's hobby crate that claims to make it easier either. In Go it's just dead simple.<p>I've noticed a lot of Rust discussion is on how to write the code or make the compiler happy which is quite telling. I don't want to spend my time thinking about how to write the code and make the compiler happy - I want to get a problem solved and a ticket closed. Hence why I went back to Go (but I acknowledge these languages have distinct goals).
I faced the same kind of issue lately and thought that implementing a From trait for each type of error was kind of annoying.<p>Taking the article example, I ended up doing this:<p><pre><code> #[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum MyError {
MyErr1,
MyErr2,
MyErr3,
}
fn read_number_from_file(filename: &str) -> Result<u64, MyError> {
let mut file = File::open(filename).or(Err(MyError::MyErr1))?; // Error!
let mut buffer = String::new();
file.read_to_string(&mut buffer).or(Err(MyError::MyErr2))?; // Error
let parsed: u64 = buffer.trim().parse().or(Err(MyError::MyErr3))?; // Error
Ok(parsed)
}
</code></pre>
As I'm a beginner, I would love to hear some thoughts on this
Something worth mentioning is that your error types do NOT have to implement std::error::Error. The type parameters on Result<T, E> have no constraints.<p>So, only your public API should implement std::error::Error.<p>Likewise, you don't HAVE TO implement From for all of your error types. In fact, it has some of the same concerns that implicit constructors in C++ have in that it's easy to just throw try-operators around without thinking about whether you actually want to bubble up an error in a specific scenario.<p>I think a lot of people who pick up Rust might not realize that and end up thinking the error defining process is even more tedious than it really is.
Beyond Borrow checker, move/copy, and error propagation(different error in fn), what else should someone learn in order to have basic understanding of Rust?
Errors in rust are one of the hardest things for me to wrap my head around as a beginner. Thanks for this post!<p>I also really like this blog post that also walks you through the history of some features like the try! Macro and shows how they are implemented <a href="https://blog.burntsushi.net/rust-error-handling/" rel="nofollow">https://blog.burntsushi.net/rust-error-handling/</a>
More hacks upon hacks to make up for the glaring inability of the Rust people to acknowledge they made a mistake by leaving out exceptions. Just like the "type after identifier" thing, the use of error values instead of exceptions was a pointless piece of Go envy in fashion at the time that we're all going to have to live with for decades.<p>Plus, Rust doesn't even get to avoid paying for exceptions, because it still has panics and still has to support unwind semantics --- even though they can be disabled at build time.<p>Avoiding exceptions was a needless technical error and an illustration of why we should design systems around tried and true techniques instead of jumping on fashion trends and ossifying those fashion trends into immutable mediocrity.