4 ms·
No, unfortunately they are not. The problem is not with checked exceptions themselves, but with the other type of exceptions in Java. In languages that rely on
by unscaled 3y ago
No, unfortunately they are not.
The problem is not with checked exceptions themselves, but with the other type of exceptions in Java.
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.
- int_19h 3y agoIt's the ergonomics of it. For a checked exception facility to not be maddening, it needs to have good facilities to propagate and wrap exceptions easily and with minimal scaffolding. But for some reason no mainstream language with traditional exception handling did that. In a similar vein, it's ironic how often you hear "composition is better than inheritance" in OO design context, and yet how few OO languages have facilities to automate delegation.
- unscaled 3y ago> In a similar vein, it's ironic how often you hear "composition is better than inheritance" in OO design context, and yet how few OO languages have facilities to automate delegation. I wholly agree with this sentiment. Rust Result types where also excruciatingly inconvenient to use at the early days, but Rust gradually added facilities to make it better: first the try! macro and if-let, then the ? operator and finally let else. Together with crates like anyhow, thiserror and eyre, error handling became a lot better. I don't use Swift a lot, but it also seem to have iterated on its error handling. In the 27 years of its existence, Java did very little to improve exception handling facilities. It added exception chaining in Java 1.4 and catching multiple exceptions in Java 7, that's it. I'm not picking up specifically on Java here - I think many languages neglect exceptions or error handling. Go is also an instructive example of a language that chose a non-exception-based error handling mechanism that the designers claimed to be superior, but failed to add ergonomics to that. This is not for the lack of trying though: the Go team tried to fix this issue multiple times, but there are very vocal parts of the Go community who opposed any kind of ergonomics, in favor of "explicitness" (as if explicitness means "error-prone boilerplate"). I would give the Go team full score for seriously trying. I give them less score on the composition-over-inheritance part though. Go is one of the languages that has objects (structs) and interfaces, but disallows inheritance, but it doesn't provide any mechanism for automating delegation. Kotlin has shown that this is possible and even quite simple. It's not one of these languages features (like method overloading, type classes and generics) that carries a lot of corner cases and complexity that you have to deal with.
- kaba0 3y agoIf all of those exceptions are custom made, then it makes sense to have a common subclass for them. It is not ideal, I agree, but it is hardly a showstopper.