What would've made them good would be a static analysis of some sort that could divine all possible exceptions thrown by a method call and its dependents based on its code, but in Java a class can always override another class and do something entirely different so that's not possible.
Checked exceptions are (for the most part) the mirror image of checked returns! Both describe and circumscribe your expectations for what comes back from the method, the only difference is one is happier than the other.
They follow the same kinds of type-rules, and the real problem is entirely on the human end: It's harder to imagine failure scenarios, features are prioritized over error-handling, and people don't like to do it if they can avoid it somehow.
> What would've made them good would be a static analysis of some sort that could divine all possible exceptions thrown by a method call and its dependents based on its code
Again, ask yourself "why don't we already do this but with return types?" Probably because it's horribly impractical in most cases and and outright impossible in others. For example, when it comes to interfaces, abstract-methods, or overridden methods... All of those involves concrete implementations that haven't been made yet.
Heck, they might not get created until months or years after you ship your .class files. Then some random guy you've never met says: "Oh, hey, I can fix my problem by creating my own implementation based on their interface..."
So unless your compiler can literally see into the future, you've got to set down your type-expectations first.
> but in Java a class can always override another class
No, even if all classes were marked "final", you'd have the same problem when it comes to using interfaces.
Furthermore, checked exceptions force you to bubble up implementation details all the way up to the interfaces. For example, if any implementation of an interface might throw IOException, you need to put that in the method signature in the interface. You can be clever and wrap it in a custom exception that exposes the same level of abstraction as the interface, but that's not a whole lot better than just using RuntimeException, and it causes people to try to cast the exception cause.
In both cases it's about constraining and describing the results from a method. The only difference is that most developers are so much more accustomed to thinking and coding along the happy-path, where they will willingly do all the exact same architectural tasks.
Seriously, read these three variations aloud and see how they sound:
>> return-types force you to bubble up implementation details all the way up to the interfaces
>> if any implementation of an interface might return Foo, you need to put that in the method signature in the interface
>> You can be clever and wrap it in a custom return-type that exposes the same level of abstraction as the interface, but that's not a whole lot better than just using Object
I'm willing to bet you can think of a slightly-incredulous answer to each of those, right? Perhaps something along the lines of "no they don't", "yes that's normal" and "WTF of course you should."
If every kind of exception was checked, then every method in Java, including constructors, would need to declare that it throws RuntimeException and IOException and NullPointerException and NoClassDefFoundError and InterruptedException because those things could happen in any method.
So we settle for declaring the type a function is expected to return, with the understanding that if something exceptional occurs a magical thing called an Exception will be thrown and it's not handled the same way as a return value. This lets us get our jobs done. It's engineering, not science.
When you solve the halting problem we can talk about formalizing every possible failure state as a return value. Until then we will get on with our lives.
Hold it: I said nothing remotely like that. You have begun ranting against an absolutist straw-man of your own creation.
What I DID say is that:
1. Type-checked exceptions are just as valuable as type-checked returns.
2. The "problems" you listed with checked-exceptions are baseless: Each of them is identical to an architectural decision or task you already regularly complete for happy-path return-types.
3. The fundamental driver of complaints is that people don't like to spend time on the unhappy-path, and aren't as rewarded for it either.
While they may not have been the wrong decision, empirical evidence continually shows they add a syntactical burden many devs just work around (e.g. just wrap in a RuntimeException anyways). It's understandable why most modern languages don't consider predictable exceptions as part of the function signatures (though many discourage exceptions altogether in all but the most extreme cases).
Error conditions captured in the type system are a vital part of making code robust. The fact that some developers don't know how to properly handle these error cases doesn't change anything to the soundness of checked exceptions.
It's not like saying they shouldn't return them (unchecked exceptions exist), it's like saying they shouldn't be forced to check the error code or declare that they will return that error code themselves. Checked exceptions aren't sound when in practice the same ones are reused to mean many things. And we shouldn't pretend deciding whether an exception should or shouldn't be checked is an objective choice applied the same way across the ecosystem.
What's not to like about that?
Contrast with all the other approaches (runtime exceptions, return codes, Either/Result, etc...) where the developer can happily ignore errors, the code will compile, and then crash at runtime.
list.stream().map(doStuff)
might throw whatever doStuff might throw, but instead map is defined so that doStuff can never throw any checked exception, which requires smuggling out every real-world failure mode.One of the parts of Lombok that really works well for me is that its notations are completely optional and fit many different levels of granularity as you see fit. Don't want sneaky exceptions? Don't use them!
I mostly end up using it to make my Java code feel like C# though...
EDIT: There's also the fact that Java includes unchecked exceptions, so you can already subvert that part of the type system.
I don't really write much Java these days, but I've use at least a few libraries which use checked exceptions in a way that really sapped the fun out of the experience. (And personally I feel it is important for writing code to be fun, because little else of working with computers is.)
"Forces API clients to catch exceptions you can't handle."
My observation is that obfuscation frameworks throw a bunch of checked exceptions which are unrecoverable. Which says more about the frameworks than the language.
Hibernate: RDBMS obfuscation framework
Spring: Flow of control obfuscation framework
JAXRS: Exception obfuscation framework