Everyone complains about checked exceptions, blames them on Java, while they actually came up on CLU, Mesa, Modula-3 and C++.
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".
The hard part of checked exceptions is when they start interacting with higher order code- in Java this often happens when an interface method wants to throw. Each implementation may want to throw its own set of exceptions, but the code that just works with the interface doesn't care about any of them, and existing languages don't really give you a way to express this.
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!
Not really what I'm talking about here- that issue comes up when you want to combine multiple concrete error types in a single monomorphic call site, rather than when dealing with polymorphism.
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.)
For my work, I usually catch general exceptions and handle their aftermath, like putting the error in an error queue to be manually fixed and replayed or showing an error page. While there are domains like power plants or car software where every error must be meticulously handled, my approach suits my domain.
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.
> 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".
The problem of java's checked exceptions is not that being explicit is bad, it's that java's implementation is terrible.
Because there isn't a one size fits all solution to error handling?
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?
I'm not really talking about the "errors" ergonomics, but more about error handling.
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.
It’s not partially designed so much as the type system demands it for rust.
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.
Not sure what Rust style languages are supposed to be, however ML derived languages, and Swift, do it much more ergonomically, without needing third party crates.
The trouble with Java exceptions is that the forced error checking is rarely handled at the call site, making the code no longer linear. Calling var a = foo();bar(a); does not necessarily imply bar() will be called. Using a more functional approach, the exception can be even more explicit making it easier to reason about.
I also thought checked exceptions in Java were fantastic. They are a form of statically checked effects. I would like to see more of this sort of thing not less.
Perhaps a controversial opinion, but I think exceptions should only be thrown when something exceptional happens. Languages, like Java and Python, overuse them e.g. failing to open a file is not exceptional, but running out of memory is.
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".