This means that each function only cares about its own error, and how to generate it. And doesn’t require macros. Just thiserror.
This means that each function only cares about its own error, and how to generate it. And doesn’t require macros. Just thiserror.
I do too! I've been debating whether I should update SNAFU's philosophy page [1] to mention this explicitly, and I think your comment is the one that put me over the edge for "yes" (along with a few child comments). Right now, it simply says "error types that are scoped to that module", but I no longer think that's strong enough.
[1]: https://docs.rs/snafu/latest/snafu/guide/philosophy/index.ht...
Your answers on stack overflow and your crates have helped me so much. Thank you!
It is the most common approach, hence, status quo.
> I create one error type per function/action. I discovered this here on HN after an article I cannot find right now was posted.
I like your approach, and think it's a lot better (including for the reasons described in this article). Sadly, there's still very few of us taking this approach.
For example, let's say the function frobnicate() writes to a file and might get all sorts of file errors (disk full, no permissions, etc.). It seems like those have to be wrapped or embedded, as the article suggests.
But then you can't use the "?" macro, I think? Because wrapping or embedding requires constructing a new error value from the original one. Every single downstream error coming from places like the standard library has to go through a transformation with map_err() or similar.
You can use ? when you implement From (to get an automatic Into) which which can be as easy as a #[from] annotation with thiserror. You can manually implement From instead of inlining the map_err if you so choose. Then you are only ever using map_err to pass additional contextual information. You usually end up using ? after map_err or directly returning the result.
It may be that you do in fact have to throw your hands up, and propagate _something_ to the caller; in those cases, I find a Box<dyn Error> or similar better maintains the abstraction boundary.
The only thing I disagree with is error wrapping. While I agree that the main error should not expose dependencies, I find it useful to keep a `cause` field that corresponds to the inner error. It's important to trace the origin of an error and have more context about it. By the way, Rust's [Error] trait has a dedicated `cause` method for that!
[Error] https://doc.rust-lang.org/nightly/core/error/trait.Error.htm...
Open sum types? I’m on the fence as to whether they should be inferrable.
The whole point (in my mind at least) of type safe errors is to know in advance all if the failure modes of a function. If you share an error enum across many functions, it no longer serves that purpose, as you have errors that exist in the type but are never returned by the function.
It would be nice if the syntax made it easier though. It's cumbersome to create new enums and implement Error for each of them.
Like, function 1 fails for reason A or B. Function 2 fails for A or C. You call both functions. How do you pattern match on reason A, in the result of your function?
In Go, there’s a somewhat simple pattern for this, which is errors.Is().
struct Error {
leaf_error: A | B | C,
context: Vec<String>,
}It could be this one: https://sabrinajewson.org/blog/errors
If it internally has a `InnerFuncErr::WriteFailed` error, you might handle it, and then you don't have to pass it back at all, or you might wrap it in an `OuterFuncErr::BadIo(inner_err)`or throw it away and make `BadIo` parameterless, if you feel that the caller won't care anyways.
Errors are not Exceptions, you don't fling them across half of your codebase until they crash the process, you try to diligently handle them, and do what makes sense.
So you don't really care about the union.
Your function typically has a specific intent when it tries to call another function. Say that you have a poorly designed function that reads from a file, parses the data, opens a DB connection and stores the data.
Should I really expect an end-user to understand an error generated by diesel/postgres/wtfdb? No, most likely I want to instruct them to generate debug logs and report an issue/contact support. This is most likely the best user experience for an application. In this case, each "action" of the function would "hide" the underlying error––it might provide information about what failed (file not found, DB rejected credentials, what part of the file couldn't be parsed, etc), but the user doesn't care (and shouldn't!) about Rust type of diesel error was generated.
To answer your question specifically, I might go without something like this:
#[derive(Debug, Error, Clone)]
pub enum MyFunctionError {
#[error("unable to read data from file: {0}")]
ReadData(String),
#[error("failed to parse data: {0}")]
Parse(String),
#[error("database refused our connection: {0} (host: {1}, username: {2})")]
DatabaseConnection(String, String, String),
#[error("failed to write rows: {0}")]
WriteData(String),
}
Obviously this is a contrived example. I wouldn't use `#[from]` and just use `.map_err` to give internal meaning to error provenances. `DatabaseConnection` and `WriteData` might have come from the same underlying WTFDbError, but I can give it more meaning by annotating it.When building a library, however, yes, I most likely do want to use `#[from] io::Error` syntax and let the calling library figure out what to do (which might very well giving the user a userful error message and dumping an error log).
You would most likely have had to navigate up and down the caller chain regardless of how you scope errors.
At least this way the compiler tells you when you forgot to handle a new error case, and where.
You can avoid this issue if you deeply nest the error types (wrap on every level). It you change an error that's not deeply-matched anywhere, you only need to update the direct callers. But "deep" errors have some tradeoffs [1] too