Checked exceptions don't mean I need to handle the exception right now. They mean I need to either do that or declare throws. Declaring throws is fine it implicitly documents the code and enforces a similar requirement up the chain.
Checked exceptions aren't the default and shouldn't be.
The choice about where to handle the exception is almost always a good thing, since it reduces boilerplate in dealing with that condition. The cases where it's not a good thing are when it's either impossible to deal with the condition at all (e.g. assertion failure) or where it must obviously be handled locally (e.g. errno==EAGAIN type stuff).
The requirement to actually make the decision is a good thing in some cases (e.g. I/O), but not in other cases (e.g. out of memory) because the overhead of making this decision so often is larger than the benefit of avoiding that class of bug. It's beneficial when it's something that can only happen while doing certain well defined activities.
There are also places where there just shouldn't be an error at all. E.g. null pointers shouldn't be an error; your program should be rejected if the compiler can't prove that this is impossible through static analysis (even if that just forces the author to write assert(ptr!=0)).
I guess I'm converging on a scheme that looks like: I/O errors are checked exceptions. Out of memory is an unchecked exception. Assertion failures are panics which cannot be caught. Division by zero is prevented by static analysis. I'm not sure what should be done with EAGAIN/EWOULDBLOCK type stuff -- it doesn't really feel like those should be exceptions, but introducing a whole new "error" handling idiom just for this doesn't feel great either.
Illegal arguments can often be avoided in the first place with sensible types (although, like null pointer exceptions, this typically requires some language support), and unsupported encodings should be rare assuming widespread UTF-8.
So yes, I agree, those should all be very rare exceptions to find in a function signature.
The first 3 need to terminate your thread because something is irretrievably incorrect. The only sane thing to do is stop.
"UnsupportedEncodingException" has lots of things you can do to recover. You can try different encodings. You can request retry of someting. You can check a CRC for corruption. etc.
So, the first 3 really shouldn't litter your code. The final one probably should.
If you just pass exceptions up in throws, you end up causing cascading checked exceptions chain changes and creating a bunch of noise, which is what many times people resort to doing in practice, because they don’t have the experience and it is very rare/almost never done to have any kind of automatic enforcement/linters/static analysis that prohibits implementation detail checked exceptions from propagating past above certain levels.
It is almost never OK to propagate IOException, for example, past maybe 1 or 2 levels of private helper methods, almost never should be propagated past your class boundary and you have to think twice about adding throws to protected methods even, unless your subclass implementations are specifically implementing alternate IO calls.
So yes that’s why checked exceptions have failed. And when you actually take this extreme discipline to your code, yes you end up with a ton of try/catch handling noise and a whole lotta new exceptions classes, and it takes similar extreme discipline to always avoid poor/improper try/catch handlers. Refactoring code across certain boundaries becomes a much bigger hassle, so your end up discouraging refactoring across those boundaries, which leads to code that is more easily stale and design decisions that are much harder to back out of.
Throwing an exception is an implicit part of our contract, whether we declare it or not. Since any method can throw an exception that implementation detail is already exposed we just don't know about it.
Yes. Propagating checked exceptions through a long chain is indeed a pita and as I said before, we need to be vigilant about using them correctly. But if I have library or infrastructure code that is doing IO/SQL, I still want people to know and handle the failure correctly.
I think they were never picked up in other languages because no one likes to tidy their room. You want to write a unit test or a hello world and suddenly there's a checked exception... No fun. Also it was used for many APIs that people felt were built badly (e.g. the infamous encoding exception, URL format, etc.).
I think this is the main reason why checked exceptions and errors as values work. The alternative is having to always specify all the exceptions in documentation so the caller knows what to expect, and hope that documentation covers everything and doesn't get out of date. That also has to propagate if the caller doesn't handle all exceptions, just like checked exceptions.
It's much better to encode this directly with checked exceptions or Result<T, E>. Why people like errors as values so much but dislike checked exceptions is beyond me. They're basically the same thing with different syntax.
I agree that it takes a lot of discipline to do this properly. Is that discipline alleviated with unchecked exceptions or would you say that doing things right takes the same work, checked or unchecked?
Yes it does. You either use try-catch-catch-...-catch, or you append the new exception to the ever growing list of exceptions your function might throw. Even worse: when two functions called by the function you design can throw the same type of exception but for different reasons (eg. two functions cannot find a file, but those are different files), then, if you were a diligent and mindful programmer, you'd have to catch both and re-throw with new types, so that upstream could differentiate between the two failures, adding even more crutft into your code.
Notice that IOException is one exception, you don't need to declare FileNotFoundException because it is an IOException. There's a similar hierarchy in an SQLException.
That's part of the beauty of checked exceptions. If your throws statement becomes too long or its stack carries too deep then you know you have a problem. This would be hard to notice otherwise.