In languages that rely on Result/Either for error handling, you've got two types of errors: Typed errors (Result/Either) and untyped panics. Typed errors are supposed to be handled, possibly based on their type, while panics can be recovered from ("catched") but these are serious, unexpected errors and you're not supposed to try to handle them based on their type. Since typed errors generally need to be handled explicitly while untyped errors are unexpected, typed errors are always checked (you can't skip handling them), while untyped errors are unchecked (implicitly propagated up the stack if you don't do anything to catch them).
Java has three types of errors:
1. Checked errors, a.k.a. checked exceptions: (exceptions that inherit from Exception, but not from RuntimeException). 2. Unchecked application errors: exceptions that inherit from RuntimeException. 3. Unchecked fatal errors: exceptions that inherit from Error.
These three kinds of errors live in a confusing class hierarchy, with Throwable covering all of them and unchecked application errors being a special case of checked application errors.
Like everything else designed in the early Java days, it shows an unhealthy obsession with deep class hierarchies (and gratuitous mutability, check out initCause()!). And this is what destroyed the utility of checked exceptions in Java in my opinion.
Consider the following example: We have a purchase() function which can return one of the following errors:
- InsufficientAccountBalance - InvalidPaymentMethod - TransactionBlocked - ServerError - etc.
You want to handle InsufficientAccountBalance by automatically topping up the user's balance if they have auto top-up configured, so you're going to have to catch this error, while letting the rest of the errors propagate up the stack, so an error message could be displayed to the user.
In Rust, you would do something like this:
account.purchase(request).map_err(|err| match err {
PurchaseError.InsufficientAccountBalance(available, required) => {
account.auto_top_up(required - available)?
account.purchase(request)
}
_ => err // Do not handle other error, just let them propagate
})
In Java, you would generally do the following: try {
account.purchase(request);
} catch (InsufficientAccountBalance e) {
account.auto_top_up(e.requiredAmount - e.availableAmount);
account.purchase(request);
} catch (Exception e) {
// We need to catch and wrap all other checked exception types here
// or the compiler would fail
throw new WrappedPurchaseException(e);
}
The "catch (Exception e)" clause doesn't just catch checked exceptions now - it catches every type of exception, and it has to wrap it in another type! Of course, you can also specify every kind of checked exception explicitly, but this is way too tedious and what you get in practice is that most code will just catch a generic Exception (or worse - Throwable!) and wrap that exception or handle it the same way, regardless if it was a NullPointerException caused by a bug in code, an invalid credit card number.The worst problem of all is that once developers get used to write "catch (Exception e)" everywhere, they start doubting the values of checked exceptions: after all, most of their try clauses seem to have a generic "catch (Exception e)", so does it really matter at all of they're using checked exceptions?
This is the reality. Checked exceptions failed in Java. Most Java developers see them as nothing more than a nuisance and look for ways to bypass them. That does not necessarily mean that the concept of checked exception as a language level facility for errors has failed, but it certainly failed the way it has been implemented in Java.