Error handling in Rust

134 emschwartz 109 6/29/2025, 8:28:25 PM felix-knorr.net ↗

Comments (109)

slau · 11h 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 · 6h 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 · 4h 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!

agent327 · 28m 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 · 6m 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.

WhyNotHugo · 9h 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 · 9h ago
I suspect you're referring to this article, which is a good read indeed: https://mmapped.blog/posts/12-rust-error-handling
atombender · 9h 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.

BlackFly · 39m 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.

remram · 8h ago
"?" call "into()" automatically, which covers simple wrapping.
mananaysiempre · 9h 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/

slau · 1h ago
Yes! Thank you!
edbaskerville · 10h 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 · 9h 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 · 2h 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().

mananaysiempre · 9h 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.

akkad33 · 46m ago
Why one error type per function. That seems overkill. Can you explain the need
larusso · 10h 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 · 1h ago
A sibling reminded me of the blog post that convinced me. Here it is: https://mmapped.blog/posts/12-rust-error-handling
LoganDark · 8h ago
`thiserror` is a derive macro :)
thrance · 10h 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 · 10h ago
This is eerily reminiscent of the discussions about Java’s checked exceptions circa 25 years ago.
mananaysiempre · 9h 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 · 9h 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.
slau · 10h 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 · 10h 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?
Animats · 11h 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 · 9h 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 · 8h 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 · 9h ago
I really wish Rust had proper union types. So much ceremony over something that could be Foo | Bar | Error
kelnos · 8h 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 · 8h 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?
amluto · 6h 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 · 5h 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.
anon-3988 · 3h 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 · 8h 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 · 8h 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 · 6h 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 · 8h 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 · 6h 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 · 8h ago
Look at Zig then, as it does exactly this. However you can’t carry any context and it’s also a problem.
_benton · 8h 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.
jonstewart · 9h 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.
pjmlp · 1h 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.
pjmlp · 10h 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 · 10h 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

pjmlp · 9h 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 · 8h 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 · 1h 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.

jenadine · 10h 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.

johnisgood · 10h 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.
worik · 4h ago
> every rust project starts with cargo install tokio thiserror anyhow.

True, almost. Not mine

Makes me sad

mparis · 6h 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.

Expurple · 2h 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 · 6h ago
Thanks for using SNAFU! Any feedback you'd like to share?
devnullbrain · 9h 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 · 10h 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 · 6h 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 · 7h ago
The best parts of Rust are Haskell. You've got a lot of precedent for how you do it.
nixpulvis · 7h ago
As a bit of an aside, I get pretty far just rolling errors by hand. Variants fall into two categories, wrappers of an underlying error type, or leafs which are unique to my application.

For example,

enum ConfigError {

    Io(io::Error),

    Parse { line: usize, col: usize },

    ...
}

You could argue it would be better to have a ParserError type and wrap that, and I absolutely might do that too, but they are roughly the same and that's the point. Move the abstraction into their appropriate module as the complexity requests it.

Pretty much any error crate just makes this easier and helps implement quality `Display` and other standard traits for these types.

echelon · 6h ago
> I get pretty far just rolling errors by hand

And you don't punish your compile times.

The macro for everything folks are making Rust slow. If we tire of repetition, I'd honestly prefer checked in code gen. At least we won't repeatedly pay the penalty.

IshKebab · 10h ago
Kind of reminds me of Java checked exceptions.
xixixao · 10h 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 · 9h 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 · 10h 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 · 9h ago
Yep. I wish Rust supported proper union types. Typescript gets it right, I just don't want to be writing Javascript...
terhechte · 2h ago
The error library he seems looking for is „error_mancer“
kshri24 · 10h ago
> And so everyone and their mother is building big error types. Well, not Everyone. A small handful of indomitable nerds still holds out against the standard.

The author is a fan of Asterix I see :)

metaltyphoon · 10h ago
> The current standard for error handling, when writing a crate, is to define one error enum per module…

Excuse me what?

> This means, that a function will return an error enum, containing error variants that the function cannot even produce.

The same problem happens with exceptions.

jppittma · 9h ago
I don’t really agree with this. The vast majority of the time, if you encounter an error at runtime, there’s not much you can do about it, but log it and try again. From there, it becomes about bubbling the error up until you have the context to do that. Having to handle bespoke error type from different libraries is actually infuriating, and people thinking this is a good idea makes anyhow mandatory for development in the language.
jgilias · 11h ago
Yeah… Please no.

I’m getting a bit of a macro fatigue in Rust. In my humble opinion the less “magic” you use in the codebase, the better. Error enums are fine. You can make them as fine-grained as makes sense in your codebase, and they end up representing a kind of an error tree. I much prefer this easy to grok way to what’s described in the article. I mean, there’s enough things to think about in the codebase, I don’t want to spend mental energy on thinking about a fancy way to represent errors.

burnt-resistor · 11h ago
Yes. Macros are a hammer, but not everything is a nail.

Declarative macros (macro_rules) should be used to straightforwardly reduce repetitive, boilerplate code generation and making complex, messy things simpler.

Procedural macros (proc_macro) allow creating arbitrary, "unhygienic" code that declarative macros forbid and also custom derive macros and such.

But it all breaks down when use of a library depends too much on magic code generation that cannot be inspected. And now we're back to dynamic language (Ruby/Python/JS) land with opaque, tinkering-hostile codebases that have baked-in complexity and side-effects.

Use magic where appropriate, but not too much of it, is often the balance that's needed.

o11c · 6h ago
Rust is trying very hard to compete with C++. That includes giving everyone a hammer so that every problem can be a thumb.
quotemstr · 10h ago
> Yes. Macros are a hammer, but not everything is a nail.

Overuse of macros is a symptom of missing language capabilities.

My biggest disappointment in Rust (and probably my least popular opinion) is how Rust botched error handling. I think non-local flow control (i.e. exceptions) with automated causal chaining (like Python) is a good language design point and I think Rust departed from this good design point prematurely in a way that's damaged the language in unfixable ways.

IOW, Rust should have had _only_ panics, and panic objects should have had rich contextual information, just like Java and Python. There should also have been an enforced "does not panic" annotation like noexcept in C++. And Drop implementations should not be allowed to panic. Ever.

God, I hope at least yeet gets in.

Expurple · 1h ago
> Overuse of macros is a symptom of missing language capabilities.

Agree.

> I think non-local flow control (i.e. exceptions) with automated causal chaining (like Python) is a good language design point

Stronly disagree: https://home.expurple.me/posts/rust-solves-the-issues-with-e...

> There should also have been an enforced "does not panic" annotation like noexcept in C++.

noexcept DOES NOT mean that the function can't throw an exception! It just means that, when it does, it aborts the program instead of unwinding into the calling function. Quoting cppreference [1]:

> Non-throwing functions are permitted to call potentially-throwing functions. Whenever an exception is thrown and the search for a handler encounters the outermost block of a non-throwing function, the function std::terminate is called

> And Drop implementations should not be allowed to panic. Ever.

Should `panic=abort` panics be allowed in Drop? They are effectively the same as std::process::exit. Do you want to mark and ban that too?

[1]: https://en.cppreference.com/w/cpp/language/noexcept_spec.htm...

quotemstr · 48m ago
> Stronly disagree: https://home.expurple.me/posts/rust-solves-the-issues-with-e...

50% at least of these tiresome "here's why exceptions suck" articles begin by talking about how "try" is un-ergonomic. The people writing these things misunderstand exceptions, probably never having actually used them in a real program. These writers think of exceptions as verbose error codes, and think (or pretend to think) that using exceptions means writing "try" everywhere. That's a strawman. Exceptional programs don't need error handling logic everywhere.

The article's author even admits at the end that Rust's error handling is garbage and forces programmers to do manually ("best practices around logging" --> waste your brain doing a computer's work) what languages with decent exception systems do for you.

> noexcept DOES NOT mean that the function can't throw an exception! It just means that, when it does, it aborts the program instead of unwinding into the calling function

Well, yeah. It means the rest of the program can't observe the function marked noexcept throwing. No... except. Noexcept. See how that works?

> Should `panic=abort` panics be allowed in Drop? They are effectively the same as std::process::exit. Do you want to mark and ban that too?

Aborting in response to logic errors is the right thing to do, even in destructors.

Expurple · 3m ago
I'm the author of this article, btw :)

> 50% at least of these tiresome "here's why exceptions suck" articles begin by talking about how "try" is un-ergonomic.

Idk about the other articles, but mine doesn't begin with that. The first argument in the article is this: "exceptions introduce a special try-catch flow which is separate from normal returns and assignments" (when there's no entrinsic reason why errors shouldn't be returned and assigned normally). The first mentioned implication of that is ergonomics, but I immediately follow up with the implications regarding reliability and code clarity. See the "Can you guess why I used an intermediate variable" toggle. Later, I also bring up a separate disadvantage of having to manually document thrown unchecked exceptions.

> The people writing these things misunderstand exceptions, probably never having actually used them in a real program.

I've supported C++ and Python applications in production.

> pretend to think that using exceptions means writing "try" everywhere.

Nope: "[propagation] is a very common error-handling pattern, and I get why people want to automate it".

> That's a strawman. Exceptional programs don't need error handling logic everywhere.

Where does the article say otherwise? You're the one pulling a strawman here.

> The article's author even admits at the end that Rust's error handling is garbage

You're free to make that conclusion. In the end, the tradeoff is subjective. But it's not the conclusion that I make in the article.

> forces programmers to do manually ("best practices around logging" --> waste your brain doing a computer's work)

That's true. But languages with unchecked exceptions force you to manually check the documentation of every method you call, in order to see whether it can throw any exceptions that you're interested in catching. And that documentation can simply be incorrect and let you down. And the set of exceptions can silently change in the next version of the library (the compiler won't tell you). And refactoring your code can silently break your error handling (the compiler won't tell you). And manually verifying the refactoring is really hard because you can't use local reasoning (`catch` is non-local and "jumps" all across the layers of your app).

It's a tradeoff.

zbentley · 9h ago
> Rust should have had _only_ panics, and panic objects should have had rich contextual information, just like Java and Python.

It could have gone that way, but that would have “fattened” the runtime and overhead of many operations, making rust unsuitable for some low-overhead-needed contexts that it chose to target as use-cases. More directly: debug and stack info being tracked on each frame has a cost (as it does in Java and many others). So does reassembling that info by taking out locks and probing around the stack to reassemble a stack trace (C++). Whether you agree with Rust’s decision to try to serve those low-overhead niches or not, that (as I understand it) is a big part of the reason for why errors work the way they do.

> There should also have been an enforced "does not panic" annotation like noexcept in C++. And Drop implementations should not be allowed to panic.

I sometimes think that I’d really love “nopanic”. Then I consider everything that could panic (e.g. allocating) and I like it less. I think that heavy use of such a feature would lead to people just giving up and calling abort() in library code in order to be nopanic-compatible, which is an objectively worse outcome than what we have today.

quotemstr · 9h ago
> debug and stack info being tracked on each frame has a cost

So add an option not to collect the debugging information. The core exception mechanism remains.

> Whether you agree with Rust’s decision to try to serve those low-overhead niches or no

It's not a matter of Rust choosing to serve those niches or not. It's the language designers not adequately considering ways to have exceptions and serve these niches. There's no contradiction: it's just when Rust was being designed, it was _fashionable_ to eschew exceptions.

> Then I consider everything that could panic (e.g. allocating) and I like it less. I think that heavy use of such a feature would lead to people just giving up and calling abort() in library code in order to be nopanic-compatible,

Huh? We don't see people write "noexcept" everywhere in C++ to be noexcept-compatible or something. Nopanic is for cleanup code or other code that needs to be infallible. Why would most code need to be infallible? I mean, panic in Drop is already very bad, so Rust people know how to write infallible code. The no-failure property deserves a syntactic marker.

dontlaugh · 8h ago
In anything performance sensitive like OSes or games, C++ is compiled without exceptions. Unwinding is simply unacceptable overhead in the general case.

Rust got errors right, with the possible exception of stdlib Error types.

quotemstr · 8h ago
Table based unwinding is just one implementation choice. You can make other choices, some of which compile to code similar to error values. See Herb Sutter's deterministic exception proposal.

https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p07...

Your post is a fantastic example of the problem I'm talking about: you're conflating a concept with one implementation of the concept and throwing away the whole concept.

Language design and implementation are different things, and as an industry, we used to understand that.

Waterluvian · 11h ago
Agreed about magic.

Please correct me if I’m misunderstanding this, but something that surprised me about Rust was how there wasn’t a guaranteed “paper trail” for symbols found in a file. Like in TypeScript or Python, if I see “Foo” I should 100% expect to see “Foo” either defined or imported in that specific file. So I can always just “walk the paper trail” to understand where something comes from.

Or I think there was also a concept of a preamble import? Where just by importing it, built-ins and/or other things would gain additional associated functions or whatnot.

In general I just really don’t like the “magic” of things being within scope or added to other things in a manner that it’s not obvious.

(I’d love to learn that I’m just doing it wrong and none of this is actually how it works in Rust)

57473m3n7Fur7h3 · 11h ago
It’s a bit confusing sometimes with macros that create types that don’t seem to exist. But usually when I work with code I use an IDE anyway and “go to definition” will bring me to where it’s defined, even when it’s via a macro.

Still generally prefer the plain non-macro declarations for structs and enums though because I can easily read them at a glance, unlike when “go to definition” brings me to some macro thing.

itishappy · 11h ago
Doesn't something like the following break the trail in pretty much all languages?

    from package import *
Waterluvian · 11h ago
Yup. And I’ve not seen that used in forever and it’s often considered a linting error because it is so nasty.

So maybe what I’m remembering about Rust was just seeing a possible but bad convention that’s not really used much.

jgilias · 11h ago
You can import everything from a module with a *, but most people seem to prefer to import things explicitly. But, yes, you can generally figure out easily where things are coming from!
throwaway894345 · 10h ago
Both sides have been a pain for me. Either I’m debugging macro errors or else I’m writing boilerplate trait impls all day… It feels like a lose/lose. I have yet to find a programming language that does errors well. :/
cute_boi · 11h ago
Rust should seriously stop using macros for everything.
nixpulvis · 6h ago
What's wrong with macros?

Especially attribute style macros which apply to variants of an enum allow a lot of code expansion which reduces boilerplate a lot.

pjmlp · 1h ago
Increase of compile times, on a already blazing fast compiler.

Debugging, especially when it lacks the tooling of a Lisp or Scheme REPL/IDE.

kaashif · 10h ago
I agree with this. It appears that in this case, macros are a band-aid over missing features in the type system.

I feel like structural typing or anonymous sum types would solve this problem without macros.

I mean given A = X | Y and B = X | Y | Z, surely the compiler can tell that every A is a B?

estebank · 9h ago
This has problems as soon as it hits generics and lifetimes.

That being said, I think a limited version of the feature that disallows the the same type with divergent lifetimes or even all generics would still be useful and well liked.

drewlesueur · 10h ago
Is it just me or is the margin/padding altered. I notice this article (being first) is squished up against the orange header bar
forrestthewoods · 9h 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 · 1h 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
burnt-resistor · 11h ago
To skip recreating OOP in Rust, use anyhow instead.
andrewmcwatters · 9h 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 · 2h 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

pjmlp · 1h 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.
fwip · 11h ago
This looks nice, especially for a mature/core library.

If your API already maps to orthogonal sets of errors, or if it's in active development/iteration, you might not get much value from this. But good & specific error types are great documentation for helping developers understand "what can go wrong," and the effects compound with layers of abstraction.

jgilias · 11h ago
Is it really though? What’s the point of having an error type per function? As the user of std::io, I don’t particularly care if file couldn’t be read in function foo, bar, or baz, I just care that the file couldn’t be read.
dwattttt · 11h ago
An error type per function doubles as documentation. If you treat all errors as the same it doesn't matter, but if you have to handle some, then you really care about what actual errors a function can return.
jgilias · 11h ago
Ok, that’s a valid point! Though there’s a trade-off there, right? If both bar and baz can not find a file, they’re both going to return their own FileNotFound error type. And then, if you care about handling files not being found somewhere up the stack, don’t you now have to care about two error types that both represent the same failure scenario?
magicalhippo · 8h ago
I don't know Rust, but I really liked Boost.System's approach[1], which was included in C++11, and have used that scheme in other languages.

The point there is the error is not just an error in isolation, but it has an attached error category as well. And the error categories can compare errors from other categories for equivalence.

So for example, say you have an error which contains (status_404, http_result_category), then you can compare that instance with (no_such_file_or_directory, generic_category), and because http_result_category knows about generic_category, it can handle the comparison and say that these are equivalent[2].

This allows you to preserve the quite detailed errors while also using them to handle generic error conditions further up.

That said, doing this for every function... sounds tedious, so perhaps not worth it.

[1]: https://www.boost.org/doc/libs/1_88_0/libs/system/doc/html/s...

[2]: https://www.boost.org/doc/libs/1_88_0/libs/system/doc/html/s...

dwattttt · 9h ago
A framing about the problem I don't often see is: when do you want to throw away information about an error case? Losing that information is sometimes the right thing to do (as you said, maybe you don't care about which file isn't found by a function, so you only use one error to represent the two times it can happen).

Maybe it would make sense to consider the API a function is presenting when making errors for it; if an error is related to an implementation detail, maybe it doesn't belong in the public API. If an error does relate to the public function's purpose (FileNotFound for a function that reads config), then it has a place there.

Groxx · 10h ago
if you have multiple files that are read in a function, and they might lead to different error handling... then sometimes yeah, perhaps they should be different types so you are guaranteed to know that this is a possibility, and can choose to do something specific when X happens. or to be informed if the library adds another file to the mix.

it isn't always the case, of course, but it also isn't always NOT the case.

im3w1l · 5h ago
To me this is backwards. I don't think there is a common need to handle a generic file not found error. Let's say the user tries to open an image file. The image file exists, but when decoding the image file you need to open some config file which happens to be missing. That needs entirely different handling than if the image file itself was missing.

Though, I suppose with something broader like IOException the situation is different.

   try {
       open file
       read some bytes
       read some more bytes
   }
makes sense, as they all relate to the same underlying resource being in a good state or not.
tayo42 · 8h ago
This article and comment section are making me feel like one of the only people that like error handling in Rust? I usually use an error for the crate or application with an enum of types. Maybe a more specific error if it makes sense. I don't even use anyhow or this error.

I like it better then python and go.

stephenlf · 10h ago
I apologize for this sidebar. I don’t have much to contribute to the technical content. It was an interesting read.

You use, too many, commas, in your, writing. It’s okay to have a long sentence with multiple phrases.

Thanks for sharing your thoughts.