And at the end of the day, it turns out forced error checking is an idea everyone keeps reinventing, because it is actually useful to know what errors can be "thrown".
And at the end of the day, it turns out forced error checking is an idea everyone keeps reinventing, because it is actually useful to know what errors can be "thrown".
What you really want here is polymorphism in the exception specification, and a way for consumers to allow these new exceptions types to "tunnel" through out to the caller that knows about them. And for that to work while still providing any guarantees that exceptions are handled, it turns out you need something like a borrow checker, to prevent the interface object from escaping that outer scope!
Here's a 2016 paper on the subject (with Barbara Liskov as a coauthor): https://www.cs.cornell.edu/~yizhou/papers/exceptions-pldi201....
Rust (and other languages that use something like `Result` for errors) handles the polymorphic case a bit better than Java, because you can already use the language's usual polymorphism support for the error type. But it's still not quite as smooth as first-class polymorphic checked exceptions would be, since the interface (or trait) and its callers have to do a bit more manual plumbing in some cases.
For example, a simple `impl Fn() -> T` can be instantiated with `T = Result<X, E>`, but only if the caller is just going to return the `T` directly- otherwise the error won't be propagated immediately. A slightly more annoying situation is when you have some `I: Iterator` that can fail- often you fall back to `I: Iterator<Item = Result<X, E>>`, which is not quite right, and expect the consumer to stop calling `next` if it gets an `Err`.
With polymorphic checked exceptions, you could use `I: Iterator<Item = X>`, with an additional annotation that `next` may fail with an `E`. Error-oblivious combinators like `map` or `fold` would continue to work directly with `X` values, but automatically propagate `E`s to the eventual caller that knows the concrete type of `I`.
(And again, crates like anyhow/thiserror don't really address this problem- they're solving a different issue entirely.)
When I see code making me catch numerous unique exceptions, it often hints at an API design issue. A more refined design might encapsulate such information in a response, maybe through a discriminated union or an enum with a message. If I can, I'd refactor it to match this ideal. If not, I'd use adapters to convert diverse exceptions into standardized errors.
Exceptions should be exceptional: situations like memory shortages, database disconnects, or accessing a disposed resource. For these unforeseen events, control flow is typically the same as they're unexpected and beyond my control.
The problem of java's checked exceptions is not that being explicit is bad, it's that java's implementation is terrible.
Because before anyhow/thiserror became popular there were 5 other popular crates for handling errors and if Rust added one of those to std we would be stuck with a subpar solution forever?
Because cargo is so easy to use that your preferred error handling solution is one `cargo add` command away?
Because for simple cases the tools the standard library offers are good enough?
The methods like .map .map_err .and_then I feel are way easier to reason about and also often shorter that what would happen in a control flow breaking catch block.
In rust, before anyhow and thiserror, you’d see some pretty shitty hacks for the inflexible error system, such as just making all errors just a string.
It is clear that having all the errors in a list is actually good now, but that doesn’t stop programmers from hating writing boilerplate.
Again, before anyhow, if you did error properly, your errors.rs had huge swathes of From implementations. Error.rs boilerplate often outstripped your actual code.
The complaints that it’s hard to change interfaces is bad, as it’s difficult to change interface methods regardless.
Very unfortunately for rust, making errors not just maddening boilerplate forces you to trade compile time for reasonable errors (although, honestly, anyhow “feels” hacks to me). Compile time is already a place rust struggles as it is.
I wouldn’t bank on rust style languages having any semblance of good ergonomics for errors. But at the same time “you can just ignore it” is really not great either.
Zig errors are actually pretty nice to work with, but as is pointed out, they struggle with producing really good messages, or giving more information back.although, I will say that I nearly never need to send more information back, and there are patterns to help with that.
Still, if there was a language concept for it, that would be nicer. It’s actually not an easy problem for zig and the core foundations of the language. Just like it’s not an easy problem for rust and its core foundations.
Errors are just really shitty and, as yet, I don’t think there exists good ergonomics. I personally haven’t seen a language that does them well.