First, the library author cannot reasonably define what is and isn't a checked exception in their public API. That really is up to the decision of the client. This wouldn't be such a big deal if it weren't so verbose to handle exceptions though: if you could trivially convert an exception to another type, or even declare it as runtime, maybe at the module or application level, you wouldn't be forced to handle them in these ways.
Second, to signature brittleness, standard advice is to create domain specific exceptions anyways. Your code probably shouldn't be throwing IOExceptions. But Java makes converting exceptions unnecessarily verbose... see above.
Ultimately, I love checked exceptions. I just hate the ergonomics around exceptions in Java. I wish designers focused more on fixing that than throwing the baby out with the bathwater.
Personally I use checked exceptions whenever I can't use Either<> and avoid unchecked like a plague.
Yeah, it's pretty sad Java language designer just completely deserted exception handling. I don't think there's any kind of improvement related to exceptions between Java 8 and 24.
To me they seem completely isomorphic?
But after experimenting a bit with checked exceptions, I realized how neglected exceptions are in Java. - There's no other way to handle checked exceptions other than try-catch block - They play very badly with API that use functional interfaces. Many APIs don't provide checked throws variant - catch block can't use generic / parameterized type, you need to catch Exception or Throwable then operate on it at runtime
After rolling my own Either<L,R>, it felt like a customizable typesafe macro for exception handling. It addresses all the annoyances I had with checked exception handling, and it plays nicely with exhaustive pattern matching using `sealed`.
Granted, it has the drawback that sometimes I have to explicitly spell out types due to local type inference failing to do so. But so far it has been a pleasant experience of handling error gracefully.
try/catch has significantly more complex call sites because it affects control flow.
Semantically from CS point of view in language semantics and type system modelling, they are equivalent in puporse, as you are very well asking about.
Can we build tools that helps us work with the boundary between isosemantic and isomorphic? Like any two things that are isosemantic should be translatable between each other. And so it represents an opportunity to make the things isomorphic.
https://news.ycombinator.com/item?id=44551088
https://news.ycombinator.com/item?id=44432640
> Your code probably shouldn't be throwing IOExceptions. But Java makes converting exceptions unnecessarily verbose
The problem just compounds too. People start checking things that they can’t handle from the functions they’re calling. The callers upstream can’t possibly handle an error from the code you’re calling, they have no idea why it’s being called.
I also hate IOException. It’s so extremely unspecific. It’s the worst way to do exceptions. Did the entire disk die or was the file not just found or do I not have permissions to write to it? IOException has no meaning.
Part of me secretly hopes Swift takes over because I really like its error handling.
So Java's checked exceptions force you to write verbose and pointless code in all the wrong places (the "in the middle" code that can't handle and doesn't care about the exception).
It doesn't, you can just declare that the function throws these as well, you don't have to handle it directly.
This is annoying enough to deal with in concrete code, but interfaces make it a nightmare.
To solve this, Rust does allow you to just Box<dyn Error> (or equivalents like anyhow). And Go has the Error interface. People who list out all concrete error types are just masochists.
It took until version 1.13 to have something better, and even now too many people still do errors.New("....."), because so is Go world.
A problem easily solved by writing business logic in pure java code without any IO and handling the exceptions gracefully at the boundary.
Think of the complaints around function coloring with async, how it's "contagious". Checked exceptions have the same function color problem. You either call the potential thrower from inside a try/catch or you declare that the caller will throw an exception.
Incidentally, for exceptions, Java had (b), but for a long time didn't have (a) (although I think this changed?), leading to (b) being abused.
That's the point! The whole reason for checked exceptions is to gain the benefit of knowing if a function starts throwing an exception that it didn't before, so you can decide how to handle it. It's a good thing, not a bad thing! It's no different from having a type system which can tell you if the arguments to a function change, or if its return type does.
And if you change a function deep in the call stack to return a different type on the happy path? Same thing. Yet, people don't complain about that and give up on statically type checking return values.
I honestly think the main reason that some people will simultaneously enjoy using Result/Try/Either types in languages like Rust while also maligning checked exceptions is because of the mental model and semantics around the terminology. I.e., "checked exception" and "unchecked exception" are both "exceptions", so our brains lumped those two concepts together; whereas returning a union type that has a success variant and a failure variant means that our brains are more willing lump the failure return and the successful return together.
To be fair, I do think it's a genuine design flaw to have checked and unchecked exceptions both named and syntactically handled similarly. The return type approach is a better semantic model for modelling expected business logic "failure" modes.
In fact, at each layer, if you want to propagate an error, you have to convert it to one specific to that layer.
Checked exceptions work well for occasional major "expectable" failures -- opening a file, making a network connection.
They work extremely poorly when required for ongoing access or use of IO/ network resources, since this forces failures which are rare & impossible to usefully recover from to be explicitly declared/ caught/ rethrown with great verbosity and negative value added.
All non-trivial software is composition, so the idea of calling code "recovering" from a failure is at odds with encapsulation. What we end up with is business logic which can fail anywhere, can't recover anything, yet all middle layers -- not just the outer transaction boundary -- are forced to catch or declare these exceptions.
Requiring these "technical exceptions" to be pervasively handled is thus not just substantially invalid & pointless, but actually leads to serious rates of faulty error-handling. Measured experience in at least a couple of large codebases is that about 5-10% of catch clauses have fucked implementations either losing the cause or (worse) continue execution with null or erroneous results.
https://literatejava.com/exceptions/checked-exceptions-javas...
I also think its a bit cleaner to have a nicely pattern matched handler blocks than bespoke handling at every level. That said, if unwrapped error results have a robust layout then its probably pretty equivalent.
Maybe you mean requests are failing on uncaught exceptions, in which case I'd say it's working well.
Or if they are unable to work, because they keep getting a maintenance page, as the load balancer redirects them after several HTTP 500 responses.
Anyway, you prefer critical workflow like payment to show a success but actually be an unhandled error?
I prefer an happy customer, and not having to deal with support calls.
Checked exceptions feel like a bad mix of error returns and colored functions to me.
But for one, Java checked exceptions don't work with generics.