I agree with you. It's funny that probably Java exceptions are famously hated on in the programming community while fulfilling those exact points as well. I'd much rather deal with too verbose exceptions (anyone seen them from Jenkins maybe?) than the minimalism that is the default in many languages nowadays.
The issue with Java exceptions is you get a lot of APIs that just "throw IOException" like this API to parse a JSON string [1]. Note that JsonParseException is a seperate exception and IOException mentions in the API docs that it's caused by "network errors or unexpected end of file", which is impossible here (missing close brackets are a JsonParseException).
It's because some nested implementation detail API several layers done can take arbitrary streams, and the String based implementation is done by creating a stream for that string, but the stream api allows for IOExceptions in the case of streams from disk or network devices etc.
What reasonable recovery option is there then? You can swallow the exception, because it's impossible for now, but your linter/teammates will complain at you and it could change in future to be possible since it's in the API contract. You could have your application re-raise the exception, propagating the problem further. Neither option is great.
It asserts "I believe this error can never happen - so if it does, just re-raise it and it'll get to the top level and the request or program will terminate - which isn't a problem, as, as I've said, this can never happen".
As we all know here, sometimes things that can never happen do happen e.g., as you stated, you initially wrote the program knowing it took a stream based on a string, but later that got changed to something that reads from a network. Having that error raised and logged top-level is the ideal outcome if such a mistake is made.
This syntax is verbose and I do wish there were a better syntax (e.g. Lombok @SneakyThrows is a hack which improves the situation somewhat) but the approach is the correct one.
And it also shows that you've thought about the exception. I even often find myself putting a little comment, mainly meant for future developers reading the code to whom it might not be obvious why this could never happen:
// Can never happen: Stream is always a string
catch (IOException e) { throw new RuntimeException(e); }
Almost. The right answer is to throw new JsonParseException(e) instead. That says that yes, this exception can happen, it's a JSON parse error. But the "outer" exception stack trace will lead to this catch block, which doesn't give all the information. But the exception wraps the "inner" exception, which has the stack trace to where the actual problem was detected. But the inner exception had the wrong type (IOException), so you wrap it with the outer exception to correctly convey what went wrong.
> some nested implementation detail API several layers done can take arbitrary streams, and the String based implementation is done by creating a stream for that string, but the stream api allows for IOExceptions in the case of streams from disk or network devices etc.
> What reasonable recovery option is there then?
adrianmsmith's answer seems to have it, here are some rather less useful thoughts, for good measure:
The C++-style approach might be to have a boolean template parameter in the Stream API, allowCheckedExceptions, to change the signatures accordingly. As Java lacks specialisation, I don't imagine there's a way to do anything similar, short of having the library offer two very similar Steam APIs, one that permits throwing of checked exceptions and one that doesn't.
edit I suppose the standard library could offer a wrapper around the Stream API, to only accept non-throwing streams, and to return a NonThrowingStream. Internally this wrapper would presumably use the strategy adrianmsmith explained.
Well, what this API's implementer could have done, is to catch the IOException and throw RuntimeException. That makes sense, because there is no reason during normal program operation for this API to throw IOException, so if it does it must be an internal bug in the library.
This pattern is equivalent to Rust's `unwrap` and `expect`, than can be called to transform a recoverable error into a panic.