Top
Best
New

Posted by emschwartz 5 days ago

Error handling in Rust(felix-knorr.net)
159 points | 150 comments
slau 5 days ago|
I disagree that the status quo is “one error per module or per library”. I create one error type per function/action. I discovered this here on HN after an article I cannot find right now was posted.

This means that each function only cares about its own error, and how to generate it. And doesn’t require macros. Just thiserror.

shepmaster 5 days ago||
> I create one error type per function/action

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...

nilirl 5 days ago|||
Just wanted to say, you single handedly made me a rust programmer.

Your answers on stack overflow and your crates have helped me so much. Thank you!

shepmaster 4 days ago||
You are quite welcome; thanks for the kind words!
conaclos 5 days ago|||
I accept, however this requires to create many types with corresponding implementations (`impl From`, `impl Display`, ...). This is a lot of boilerplate.
shepmaster 4 days ago|||
In addition to the sibling comment mentioning thiserror, I also submit my crate SNAFU (linked in my ancestor comment). Reducing some of the boilerplate is a big reason I enjoy using it.
colanderman 5 days ago|||
thiserror automates all of that: https://docs.rs/thiserror/latest/thiserror/ Highly recommended.
conaclos 4 days ago||
Sure, but it's an additional dependency. I would prefer it if some of this machinery were added to `core`.
WhyNotHugo 5 days ago|||
> I disagree that the status quo is “one error per module or per library”.

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.

YorickPeterse 5 days ago|||
I suspect you're referring to this article, which is a good read indeed: https://mmapped.blog/posts/12-rust-error-handling
atombender 5 days ago|||
With that scheme, what about propagating errors upwards? It seems like you have to wrap all "foreign" error types that happen during execution with an explicit enum type.

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.

remram 5 days ago|||
"?" call "into()" automatically, which covers simple wrapping.
BlackFly 5 days ago||||
The article expressly suggests not wrapping/embedding those errors but instead putting the errors into context. That suggestions starts with the sentence "Define errors in terms of the problem, not a solution" and explicitly shows a wrapping as an anti-pattern then lays out the better solution. Note that the author's solution serializes the underlying error in some cases to avoid leaking it as a dependency.

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.

colanderman 5 days ago|||
I find that directly propagating errors upwards is an antipattern. In many/most cases, the caller has less information than you do for how to deal with the error. (Your library, after all, is an abstraction, of which such an error is an implementation detail.) It also prevents you from returning your own error condition which isn't one that the downstack error type can represent.

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.

mananaysiempre 5 days ago||||
It’s a nice, well-written and well-reasoned article, and yet after shoveling through all the boilerplate it offers as the solution I can’t help but “WTF WTF WTF ...”[1].

[1] https://danluu.com/wat/

conaclos 5 days ago||||
Thanks for sharing!

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...

slau 5 days ago|||
Yes! Thank you!
edbaskerville 5 days ago|||
I thought I was a pedantic non-idiomatic weirdo for doing this. But it really felt like the right way---and also that the language should make this pattern much easier.
resonious 5 days ago|||
The "status quo" way erodes the benefit of Rust's error system.

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.

klodolph 5 days ago||
It’s not just syntax, there are semantic problems.

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().

Expurple 5 days ago||
It's a tradeoff. When you have a "flat" union like `A | B | C` that makes it easy to pattern-match "leaf" errors, you give up the ability to add any additional context in function1 and function2. Although, there are workarounds like

    struct Error {
        leaf_error: A | B | C,
        context: Vec<String>,
    }
mananaysiempre 5 days ago|||
> also that the language should make this pattern much easier

Open sum types? I’m on the fence as to whether they should be inferrable.

Expurple 5 days ago|||
> I create one error type per function/action. I discovered this here on HN after an article I cannot find right now was posted.

It could be this one: https://sabrinajewson.org/blog/errors

agent327 5 days ago|||
How does that compose? If you call somebody else's function, do you just create a superset of all possible errors they can return? What if it is a library that doesn't really specify what errors an individual function can return, but just has errors for the whole library?
j-pb 5 days ago|||
You return an error specific to that function.

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.

slau 5 days ago|||
There's a bunch of different situations that can be discussed, and it's hard to generalise. However:

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).

thrance 5 days ago|||
That's the way, but I find it quite painful at time. Adding a new error variant to a function means I now have to travel up the hierarchy of its callers to handle it or add it to their error set as well.
layer8 5 days ago|||
This is eerily reminiscent of the discussions about Java’s checked exceptions circa 25 years ago.
mananaysiempre 5 days ago||
The difference is that Java does not have polymorphism for exception sets (I think? certainly it didn’t for anything back then), so you couldn’t even type e.g. a callback-invoking function properly. Otherwise, yes, as well as the discussions about monad transformers in Haskell from 15 years ago. Effects are effects are effects.
layer8 5 days ago||
You can abstract over a finite set of exception types in Java with generics, but it doesn’t always work well for n > 1 and is a bit boilerplate-y. The discussions I was referring to predate generics, however.
Expurple 5 days ago||||
That's only the case when your per-function error sets are "flat" (directly contain "leaf" errors from many layers below).

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

[1]: https://news.ycombinator.com/item?id=44420061

slau 5 days ago||||
This shouldn’t happen unless you’re actively refactoring the function and introducing new error paths. Therefore, it is to be expected that the cake hierarchy would be affected.

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.

mdaniel 5 days ago|||
Sometimes that error was being smuggled in another broader error to begin with, so if the caller is having to go spelunking into the .description (or .message) to know, that's a very serious problem. The distinction I make is: if the caller knew about this new type of error, what would they do differently?
larusso 5 days ago|||
Have an example that I can read. I also use this error but struggle a bit when it comes to deciding how fine grained or like in the article, how big an error type should be.
slau 5 days ago||
A sibling reminded me of the blog post that convinced me. Here it is: https://mmapped.blog/posts/12-rust-error-handling
LoganDark 5 days ago|||
`thiserror` is a derive macro :)
akkad33 5 days ago||
Why one error type per function. That seems overkill. Can you explain the need
Expurple 5 days ago||
See the two articles linked in the sibling comments
Animats 5 days ago||
It's hard. Python 2.x had a good error exception hierarchy, which made it possible to sort out transient errors (network, remote HTTP, etc.) errors from errors not worth retrying. Python 3 refactored the error hierarchy, and it got worse from the recovery perspective, but better from a taxonomy perspective.

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.

efnx 5 days ago||
`Result` is just `Either` by another name, and the main idea is to use sum and product types as the result of a computation instead of throwing, which turns error handling into business as usual. The `Result` type and `Error` trait really are orthogonal, and as soon as the `Try` trait is stabilized I think we'll see some good improvements.
tialaramex 5 days ago|||
Semantics matter. This is a mistake C++ has made and will probably pay for when it eventually tries to land pattern matching. Rust knows that Option<Goose> and Result<Goose,()> are completely different semantically, likewise Result<Happy,Sad> and Either<Happy,Sad> communicate quite different intents even if the in-memory representations are identical.
_benton 5 days ago|||
I really wish Rust had proper union types. So much ceremony over something that could be Foo | Bar | Error
kelnos 5 days ago|||
Yeah, it's frustrating that there's no syntax for this. It could even be syntactic sugar; in this case if you had:

    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...

_benton 5 days ago||
Would that work with refactoring too? Like for example if a function that once returned Maybe<Foo> was refactored to return Foo, would a consumer that received Maybe<Foo> still work?
veidelis 3 days ago||
Probably yes, with From and Into traits.
amluto 5 days ago||||
I don't, and I say this as a long-time user of C++'s std::variant and boost::variant, which are effectively union types.

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!
jffaufwwasd 5 days ago||
This is where you should be combining them.

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.
amluto 4 days ago|||
Ah, so you want both sum and union types, and you probably want those unions to be genuinely unordered, so that A | B is the same as B | A. And maybe even with inferred subtype relationships or automatic conversions so that A | B can be used where A | B | C is expected. This could be useful.

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.

anon-3988 5 days ago|||
This is brilliant, I feel like Rust should ultimately arrive at this but imagine this is very hard to implement from the tooling perspective.
rtpg 5 days ago||||
I've thought about this a good amount too because Typescript gets so much out of untagged unions but I think that with a language like Rust you get into a huge mess due to it messing up inference and also Rust _really_ wanting to know the size of a type most of the time.

    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.

madeofpalk 5 days ago|||
I don't understand the ambiguity.

    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?

rtpg 5 days ago||
In TS you get literals, which is its own dimension of valuable tooling of course.

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

_benton 5 days ago|||
Well, let x = 1; x = false; is a type error in TS anyways. let x: Number | Boolean = 1 is fine but also clear about its allowed types.

Cant you do something like let mut x: Result<Either<Foo, Bar>, Error> in Rust? Same thing, just more ceremony?

rtpg 5 days ago||
Yeah my point is more about in type inference. If you explicitly annotate the expression's type then I'm not worried.

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.

metaltyphoon 5 days ago|||
Look at Zig then, as it does exactly this. However you can’t carry any context and it’s also a problem.
_benton 5 days ago||
Zig has far too many other issues tho (lack of interfaces??) for me to seriously consider it a competitor to TS's type system.
metaltyphoon 4 days ago||
I 100% agree with you. I wanted OP to see:

> 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.

jonstewart 5 days ago|||
There’s much not to like about C++ exceptions, I get it, but as a C++ programmer the proliferation of error types in Rust rubs me the wrong way. I like that C++ defines a hierarchy of exceptions, like Python, and you are free to reuse them. I do not want to go to the hassle of defining some new error types everywhere, I just want to use equivalents to runtime_error or logic_error. It feels like Rust is multiplying unnecessarily.
Expurple 5 days ago|||
> I do not want to go to the hassle of defining some new error types everywhere, I just want to use equivalents to runtime_error or logic_error.

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.

pjmlp 5 days ago|||
It isn't even the proliferation per se, rather that integration them requires macros, boilerplate code and reaching out to external crates, for what should be builtin.
Expurple 5 days ago||
The boilerplate is more meaningful than it seems: https://home.expurple.me/posts/why-use-structured-errors-in-...
pjmlp 5 days ago||
This is the kind of stuff I would rather not have outsourced for 3rd party dependencies.

Every Rust project starts by looking into 3rd party libraries for error handling and async runtimes.

wongarsu 5 days ago|
Or rather every rust project starts with cargo install tokio thiserror anyhow.

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

jenadine 5 days ago|||
I prefer `derive_more` than thiserror. Because it is a superset and has more useful derive I use.

color-eyre is better than anyhow.

pjmlp 5 days ago||||
I rather take the approach that basic language features are in the box.

Too much innovation gets out of control, and might not be available every platform.

nemothekid 5 days ago||
I think this is a valid criticism, however I think the direction Rust went was better. It's easy in hindsight to say that the error handling system that emerged in ~2020-ish should have been baked in the library when the language was stabilized in 2015. However even in 2012 (when Go 1.0 was released), errors as values was a pretty novel idea among mainstream programming languages and Go has some warts that were baked into the language that they have now given up on fixing.

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.

pjmlp 5 days ago||
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.

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.

nemothekid 4 days ago||
>However even in 2012 (when Go 1.0 was released), errors as values was a pretty novel idea among mainstream programming languages

>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.

johnisgood 5 days ago||||
Just please let us not end up with something like Node.js where we use a crate that has <10 LOC. Irks me. And if Rust ends up like that, I will never switch. It already builds ~50 crates for medium-sized projects.
Expurple 5 days ago||
Rust crates usually have "feature flags" [1] that help with dependency bloat. You only pull in the parts of the dependency tree that you actually use, even if the crates that you use can be configured to provide more features and pull more dependencies.

https://doc.rust-lang.org/cargo/reference/features.html

pjmlp 5 days ago||
Also a reason why some crates get compiled multiple times, making the compile times even more of a pain, when they happen to be referenced multiple times with different configurations.
Expurple 5 days ago||
True, but this is being worked on now: https://github.com/rust-lang/cargo/issues/14774
pjmlp 5 days ago||
Last update was almost a year ago, hardly being worked on.
Expurple 5 days ago||
What do you mean? At the bottom of the page, the latest update is a pull request that was opened 10 days ago: https://github.com/rust-lang/cargo/pull/15684
worik 5 days ago|||
> every rust project starts with cargo install tokio thiserror anyhow.

True, almost. Not mine

Makes me sad

mparis 5 days ago||
I'm a recent snafu (https://docs.rs/snafu/latest/snafu/) convert over thiserror (https://docs.rs/thiserror/latest/thiserror/). You pay the cost of adding `context` calls at error sites but it leads to great error propagation and enables multiple error variants that reference the same source error type which I always had issues with in `thiserror`.

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.

shepmaster 5 days ago||
Thanks for using SNAFU! Any feedback you'd like to share?
Expurple 5 days ago||
> multiple error variants that reference the same source error type which I always had issues with in `thiserror`.

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.
shepmaster 4 days ago||
You certainly can use thiserror to accomplish the same goals! However, your example does a little subtle slight-of-hand that you probably didn't mean to and leaves off the enum name (or the `use` statement):

    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.
forrestthewoods 5 days ago||
I quite like the Rust approach of Result and Option. The anyhow and thiserror crate are pretty good. But yeah I constantly get confused by when errors can and can not coerce. It's confusing and surprising and I still run into random situations I can't make heads or tails from.

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.

Expurple 5 days ago|
> But yeah I constantly get confused by when errors can and can not coerce.

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
forrestthewoods 3 days ago||
I regularly get super confused by what coerces and what doesn’t. I swear there’s cases where ? works but into() does not.

It’s many things. But simple ain’t one of them =D

Expurple 2 days ago||
I'm not aware of such cases. And The Rust Reference says [1]:

> 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...

devnullbrain 5 days ago||
>This means, that a function will return an error enum, containing error variants that the function cannot even produce. If you match on this error enum, you will have to manually distinguish which of those variants are not applicable in your current scope

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.

the__alchemist 5 days ago||
Lately, I've been using io::Error for so many things. (When I'm on std). It feels like everything on my project that has an error that I could semantically justify as I/O. Usually it's ErrorKind::InvalidData, even more specifically. Maybe due to doing a lot of file and wire protocol/USB-serial work?

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.

nixpulvis 5 days ago||
I think I read somewhere that anyhow is great for application code where you want a unified error type across the application. And something like thiserror is good for library code where you want specific error variants for each kind of fallibility.

Personally, I think I prefer thiserror style errors everywhere, but I can see some of the tradeoffs.

benreesman 5 days ago||
The best parts of Rust are Haskell. You've got a lot of precedent for how you do it.
IshKebab 5 days ago||
Kind of reminds me of Java checked exceptions.
Expurple 5 days ago||
It's kinda similar, but Rust also solves the problems of checked exceptions: https://home.expurple.me/posts/rust-solves-the-issues-with-e...
vmaurin 5 days ago||
The cyclic pattern of IT. New language, same problems, same mistakes, same solutions found (but losing years again)
IshKebab 5 days ago||
No, I said it reminds me of them. It definitely avoids some of the flaws of exceptions. Not the same solution.
xixixao 5 days ago||
I find TS philosophy of requiring input types and inferring return types (something I was initially quite sceptical about when Flow was adopting it) quite nice to work with in practice - the same could be applied to strict typing of errors ala Effect.js?

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).

estebank 5 days ago||
Fun fact: the compiler itself has some limited inference abilities for return types, they are just not exposed to the language: https://play.rust-lang.org/?version=nightly&mode=debug&editi...

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).

xixixao 5 days ago||
This already does work in TS, and there are some patterns besides Effect that simplify working with the return values.

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).

_benton 5 days ago||
Yep. I wish Rust supported proper union types. Typescript gets it right, I just don't want to be writing Javascript...
andrewmcwatters 5 days ago|
Go got this right. Lua also has a nice error mechanism that I haven't seen elsewhere where you can explicitly state where in the call stack the error is occurring (did I error from the caller? or the callee?).

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?

Expurple 5 days ago||
No, Go didn't get this right. Returning a tuple (a T and an errror) isn't an appropriate tool when you want your function to return either a T or an errror. It's a brittle hack that reqires everyone to use a third-party linter on top. Otherwise that tuple is handled incorrectly too frequently.

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

Expurple 4 days ago||
I've even decided to turn my parent comment into a blog post [1]. It also fixes numerous typos, so qoute the blog version.

[1] https://home.expurple.me/posts/go-did-not-get-error-handling...

andrewmcwatters 2 days ago||
I’m honored you disagree with me enough to write your thoughts on the matter. Thank you for your insight.
Expurple 2 days ago||
Can't tell if it's sarcasm or not
andrewmcwatters 2 days ago||
No, I'm not being sarcastic. It's easier to ignore a comment than it is to discuss the topic.
Expurple 2 days ago||
Well, I just have an intrinsic interest in discussing this topic deeply and understanding it deeply. So far, all of my technical posts on the blog have been about error handling
pjmlp 5 days ago||
No it didn't, because it failed to provide an alternative to the boilerplate error checking, like Odin, Rust, Swift and Zig, or monadic composition functions.
More comments...