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...
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>,
}
Open sum types? I’m on the fence as to whether they should be inferrable.
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 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
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.
Rust probably should have had a set of standard error traits one could specialize, but Rust is not a good language for what's really an object hierarchy.
Error handling came late to Rust. It was years before "?" and "anyhow". "Result" was a really good idea, but "Error" doesn't do enough.
type FooOrBarOrError = Foo | Bar | Error;
Then that could desugar to: enum FooOrBarOrError {
Foo(Foo),
Bar(Bar),
Error(Error),
}
And it could also implement From for you, so you can easily get a FooOrBarOrError from a Foo, Bar, or Error; as well as implementing Display, StdError, etc. if the components already implement them.I actually wonder if you could implement this as a proc macro...
Foo | Bar makes sense when Foo and Bar are logically similar and their primary difference is the difference in type. This is actually rather rare. One example would be a term in your favorite configuration markup language along the lines of JSON or YAML:
type Term = String | List<String>;
or perhaps a fancier recursive one: type Term = String | Box<List<Term>>;
or however you want to spell it. Here a Term is something in the language, and there are two kinds of terms: string or lists.But most of the time that I've wanted a sum type, I have a set of logical things that my type can represent, and each of those things has an associated type of the data they carry. Result types (success or error) are absolutely in that category. And doing this wrong can result in a mess. For example, if instead of Result, you have SuccessVal | Error, then the only way to distinguish success from error is to literally, or parametrically in a generic, spell out SuccessVal or Error. And there are nasty pathological cases, for example, what if you want a function that parses a string into an Error? You would want to write:
fn parse_error(input_string: &str) -> Error | Error
Whoops!Ok' and Err' as nominal type constructors which are unioned:
struct Ok<T>(T);
struct Err<E>(E);
fn parse_error(input_string: &str) -> Ok Error | Err (ErrorA | ErrorB | ErrorC...)
Or make a sum type enum Result<T, E> {
Ok(T),
Err(E),
}
fn parse_error(input_string: &str) -> Result<Error, (ErrorA | ErrorB | ErrorC...)>
The error types are unioned for easy composition but you have a top level sum type to differentiate between success and failure.I can also imagine it resulting in horrible compilation times and/or generated code bloat in a language+toolchain like Rust that insists on monomorphizing everything.
let mut x = 1
x = false
is x a usize? a bool? a usize | bool? let mut x = if some_condition { 1 } else { false }
is x x a usize? a bool? a usize | bool?One could make inference rules about never inserting unions without explicit intervention of user types. But then you get into (IMO) some messy things downstream of Rust's pervasive "everything is an expression" philosophy.
This is less of a problem in Typescript because you are, generally, much less likely to have conditionally typed expressions. There's a ternary operator, but things like `switch` are a statement.
So in production code Typescript, when presented with branching, will have a downstream explicit type to unify on. Not so much in Rust's extensive type chaining IMO. And then we start talking about Into/From and friends....
I don't really think that you want a rule like `(if cond { x: T } else { y: U}) : T | U` in general. You'll end up with _so many_ false negatives and type errors at a distance. But if you _don't_ have that rule, then I don't know how easily your error type unification would work.
let mut x = 1
x = false
In TS, x is inferred as usize, second line is an error. let mut x = if some_condition { 1 } else { false }
In TS, x is inferred as usize | bool.Is there something specific to rust that makes this less clear that I'm missing?
So inferring as an untagged union is not wrong of course! It's just that if you are always inferring the type of an if expression to A | B, then this will also happen unintentionally a lot.
And so at the end of some code, when you actually use x, then you'll see an error like "expected usize, got usize | bool". In the case that this was a mistake, you're now looking at having to manually figure out why x was inferred this way.
In typescript your "if expression" is a ternary expression. Those are quite rare. In rust they're all over the place. Match statements are the same thing. Imagine having a 10 clause match statement and one of them unintentionally gives a different type. There's even just the classic "semicolon makes the branch into a ()"!
So always inferring a union across branches of an if expression or a match means that your type errors on genuine mistakes are almost never in the right spot.
Of course we can annotate intermediate values to find our way back. Annotating intermediate values in a chained expression is a bit miserable, but it is what it is.
Decent typescript tends to not have this problem because there are few syntactic structures where you need to evaluate multiple branches to figure out the type of an expression. And the one big example (return values)... well you want to be annotating the return value of your functions in general.
Rust is in a similar space for Result types, at least. But I don't think it generalizes at all. If you start inferring union types, and combine that with trait resolution, I _think_ that we'd end up with much less helpful error messages in the case of actual mistakes, because the actual location of the error will be harder to find.
let mut x = if some_condition { 1 } else { false }
// bunch of code
return f(x) // expected usize, got usize | bool
TS gets away with this stuff because JS's object model is simple (TS doesn't need to do any form of trait resolution!) and the opportunities to introduce unions implicitly are relatively few in TS code in general.And this isn't even really getting into Rust needing to actually implement untagged unions if they had them! Implicit tagging feels off in a language very serious about not having expensive hidden abstractions. But how are you going to guarantee bit layout to allow for the differentiation here?
I'm saying all of this but I'd love it if someone showed up with a good untagged union proposal to Rust, because I _like_ the concept. Just feels intractable
Cant you do something like let mut x: Result<Either<Foo, Bar>, Error> in Rust? Same thing, just more ceremony?
Just like.... if you infer the union then all your type errors are going to shift around and you'll have to do more hunting to figure out where your stuff is. And my impression is that Rust has a lot more expression inference going on in practice than TS. But just an impression.
> So much ceremony over something that could be Foo | Bar | Error
Is not really a good idea and he can see this done in Zig.
You can use anyhow::Error everywhere. If you need to "catch" a specific wrapped error, you can manually document it on the "throwing" method and downcast where you "catch". It's very similar to exceptions (checked-but-unspecific `throws Exception` in Java), but better, because Result and ? are explicit.
Every Rust project starts by looking into 3rd party libraries for error handling and async runtimes.
If we just added what 95% of projects are using to the standard library then the async runtime would be tokio, and error handling would be thiserror for making error types and anyhow for error handling.
Your ability to go look for new 3rd party libraries, as well as this article's recommendations, are examples of how Rust's careful approach to standard library additions allows the ecosystem to innovate and try to come up with new and better solutions that might not be API compatible with the status quo
color-eyre is better than anyhow.
Too much innovation gets out of control, and might not be available every platform.
As a result, I find error handling in Go to be pretty cumbersome even though the language design has progressed to a point where it theoretically could be made much more ergonomic. You can imagine a world where instead of functions returning `(x, err)` they could return `Result[T]error` - an that would open up so many more monadic apis, similar to whats in Rust. But that future seems to be completely blocked off because of the error handling patterns that are now baked into the language.
There's no guarantee the Rust team would have landed on something particularly useful. Even the entire error trait, as released, is now deprecated. `thiserror`, the most popular error crate for libraries wasn't released until 2019.
Also as you can seen by sibling comments, the beauty of 3rd party dependencies is that each dev has a different opinion what they should be, so any given project gets a bunch of them.
>Errors as values is a quite old idea, predating exceptions, mainstream just got a bit forgotten about how we used to code until early 2000's.
I'm not sure these two sentences are in disagreement with each other.
>the beauty of 3rd party dependencies is that each dev has a different opinion what they should be,
The hope is in that some time in the future, the community will eventually coalesce on a superior option with the appropriate patterns. Because the mainstream had forgotten how to use errors as values, my point is, its more likely that Rust 1.0 would have baked in a poor solution.
True, almost. Not mine
Makes me sad
No dogma. If you want an error per module that seems like a good way to start, but for complex cases where you want to break an error down more, we'll often have an error type per function/struct/trait.
Huh?
#[derive(Debug, thiserror::Error)]
enum CustomError {
#[error("failed to open a: {0}")]
A(std::io::Error),
#[error("failed to open b: {0}")]
B(std::io::Error),
}
fn main() -> Result<(), CustomError> {
std::fs::read_to_string("a").map_err(CustomError::A)?;
std::fs::read_to_string("b").map_err(CustomError::B)?;
Ok(())
}
If I understand correctly, the main feature of snafu is "merely" reducing the boilerplace when adding context: low_level_result.context(ErrorWithContextSnafu { context })?;
// vs
low_level_result.map_err(|err| ErrorWithContext { err, context })?;
But to me, the win seems to small to justify the added complexity. low_level_result.context(ErrorWithContextSnafu { context })?;
low_level_result.map_err(|err| CustomError::ErrorWithContext { err, context })?;
Other small details:- You don't need to move the inner error yourself.
- You don't need to use a closure, which saves a few characters. This is even true in cases where you have a reference and want to store the owned value in the error:
#[derive(Debug, Snafu)]
struct DemoError { source: std::io::Error, filename: PathBuf }
let filename: &Path = todo!();
result.context(OpenFileSnafu { filename })?; // `context` will change `&Path` to `PathBuf`
- You can choose to capture certain values implicitly, such as a source file location, a backtrace, or your own custom data (the current time, a global-ish request ID, etc.)----
As an aside:
#[error("failed to open a: {0}")]
It is now discouraged to include the text of the inner error in the `Display` of the wrapping error. Including it leads to duplicated data when printing out chains of errors in a nicer / structured manner. SNAFU has a few types that work to undo this duplication, but it's better to avoid it in the first place.I don't know what the solution is. And Rust is definitely a lot better than C++ or Go. But it also hasn't hit the secret sauce final solution imho.
It's simple, really. `?` coerces errors if there's an `impl From<InnerError> for OuterError`:
fn outer() -> Result<(), OuterError> {
inner()?;
Ok(())
}
When OuterError is your own type, you can always add that impl. When it's a library type, you're at its mercy. E.g., the point of anyhow::Error is that it's designed to automatically wrap any other error. To do that, anyhow provides an impl<E> From<E> for anyhow::Error
where
E: Error + Send + Sync + 'static
It’s many things. But simple ain’t one of them =D
> If the value is Err(e), then it will return Err(From::from(e)) from the enclosing function or closure.
Without a specific example, I can't help you further.
[1]: https://doc.rust-lang.org/stable/reference/expressions/opera...
You have to anyway.
The return type isn't to define what error variants the function can return. We already have something for that, it's called the function body. If we only wanted to specify the variants that could be returned, we wouldn't need to specify anything at all: the compiler could work it out.
No. The point of the function signature is the interface for the calling function. If that function sees an error type with foo and bar and baz variants, it should have code paths for all of them.
It's not right to say that the function cannot produce them, only that it doesn't currently produce them.
On no_std, I've been doing something like the author describes: Single enum error type; keeps things simple, without losing specificity, due the variants.
When I need to parse a utf-8 error or something, I use .map_err(|_| ...)
After reading the other comments in this thread, it sounds like I'm the target audience for `anyhow`, and I should use that instead.
Personally, I think I prefer thiserror style errors everywhere, but I can see some of the tradeoffs.
This does add the “complexity” of there being places (crate boundaries in Rust) where you want types explicitly defined (so to infer types in one crate doesn’t require typechecking all its dependencies). TS can generate these types, and really ought to be able to check invariants on them like “no implicit any”.
Rust of course has difference constraints and hails more from Haskell’s heritage where the declared return types can impact runtime behavior instead. I find this makes Rust code harder to read unfortunately, and would avoid it if I could in Rust (it’s hard given the ecosystem and stdlib).
I have some desire to make an RFC for limited cross-item inference within a single crate, but part of it wouldn't be needed with stabilized impl Trait in more positions. For public items I don't think the language will ever allow it, not only due to technical concerns (not wanting global inference causing compile times to explode) but also language design concerns (inferred return types would be a very big footgun around API stability for crate owners).
Which brings me to my other big gripe with Rust (and Go): the need to declare structs makes it really unwieldy to return many values (resorting to tuples, which make code more error prone and again harder to read).
Similarly, JavaScript seems to do OK, but I miss error levels. And C seems to also have OK error conventions that aren't too bad. There's a handful of them, and they're pretty uncontroversial.
Macros seem to be wrong in every language they're used, because people can't help themselves.
It's like a red flag that the language designers were OK giving you enough rope to hang yourself with, but also actively encourage you to kill yourself because why else would you use the rope for anything else?
All of that, because Go keeps ignoring a basic feature from the 1970s [1] that allows to you express the "or" relationships (and nullability).
APIs that are easy to use incorrectly are bad APIs.
[1]: https://en.wikipedia.org/wiki/Tagged_union#1970s_&_1980s
[1] https://home.expurple.me/posts/go-did-not-get-error-handling...