I am upgrading AWS' Java SDK from 1.x to 2.x. In 2.x, it seems that most exceptional cases are modeled with Unchecked Exceptions, as opposed to Checked Exceptions like it was in 1.x.
When trying to figure out why, someone on Reddit pointed me to this snippet of AWS documentation:
The AWS SDK for Java uses runtime (or unchecked) exceptions instead of checked exceptions for these reasons:
To allow developers fine-grained control over the errors they want to handle without forcing them to handle exceptional cases they aren’t concerned about (and making their code overly verbose)
To prevent scalability issues inherent with checked exceptions in large applications
In general, checked exceptions work well on small scales, but can become troublesome as applications grow and become more complex.
Now, I don't know how small "small-scale" really is, but I have found Checked Exceptions an absolute pleasure to work with. I don't see what they mean at all by saying there is significant friction at larger scales.
What sort of friction can I expect Checked Exceptions to give me on "large-scale" applications? A couple more try
-catch
statements?
The AWS API design team didn't do a good job. These arguments are nonsense.
To allow developers fine-grained control over the errors they want to handle without forcing them to handle exceptional cases they aren’t concerned about (and making their code overly verbose)
This is gobbledygook. Giving it my utter best effort at being a sensible statement, then, the complaint boils down to more or less:
throws
clause that lists lots of exceptions. 5, maybe 20.But hopefully that's not actually what they mean because that's ridiculous - you can make supertypes. It could just throws AwsException
. See for example how the cryptographic APIs of java itself have a boatload of exceptions, but they all extend GeneralSecurityException
(note the large amount of subclasses listed). IOException is similar; it has plenty of subtypes.
For truly exotic situations that really only occur for a single API endpoint, if for some reason "I have 150 distinct checked exception types" is unmaintainable (I don't really see why that's actually a problem, but lets roll with it), exception types are actual, full java types. You can give them an enum or similar to convey a well known error code. For example, SQLException's own getSQLState()
implements this strategy. I don't recommend this - catch (DuplicateKeyException e)
is a lot nicer than catch (SQLException e) { if e.getSQLState().equals("1200F") {
- but it's a solution if, for example, the authors of the types are a separate team from the authors of the API itself.
To prevent scalability issues inherent with checked exceptions in large applications
Nonsense.
Large applications do not scale if you treat them as a big mess o spaghetti. The way to scale large application designs is modularization. And not in the 'use the jigsaw stuff (module-info)' sense - more abstract than that. If you draw the complete design of your system out on a whiteboard, it should be possible to separate the whole thing into a bunch of components that are almost independent - draw a few big circles around these components, and the idea is that the amount of 'lines' that flow between the big circles is very small. This way, a given component can be understood on its own and e.g. the learning curve when onboarding a new team member is better than 'well, spend 3 years getting familiar with all of this 15-years-in-continuous-use, 4 million lines of code project'.
Such a component then acts like a small/medium sized app for most purposes. Hence the very nature of the argument 'it works for small stuff but fails for big stuff' is suspect right out of the gate, and we haven't even gotten to the 'this is an argument about checked exceptions' part.
It's even weaker once that's brought in. Here's an example of what they are presumably talking about:
class GameState {
public void saveGame(String saveName) throws SQLException {
...
}
}
//NB: This is bad design used as an example.
The problem here is that in this 'large' app (a game, of some sort), the code that wants to trigger 'save game' now is all of a sudden confronted with the fact that, evidently, databases are involved. Other than the throws SQLException
part, imagine that the code you were writing that wants to call this saveGame
has never run into 'oh, this thing uses databases somewhere'. Now the mere existence of that throws
clause changes the rules and 'impacts scalability of the development process' because a bunch of code is now being written where the author needs to now first figure out how that DB is involved.
Except, this is trivially avoidable, and the above is simply bad design. There are only 2 options:
But, the code as written above is badly designed if it's the second case. This is how the code should be written:
class GameState {
void saveGame(String saveName) throws SaveException {
try {
// impl detail that writes to a DB
} catch (SQLException e) {
throw new SaveException("Explain stuff", e);
}
}
public static class SaveException extends Exception { .. }
}
Yes, this is more code, but the amount of code you should be writing isn't affected if you change to RuntimeExceptions - giving separate error conditions separate exception types is fundamental if you want to scale your application; the ability for code to respond specifically to a given error condition can't be done if everybody writes throw new RuntimeException(...)
for every error that could occur.
And that 'more code' thing is, in fact, what allows you to scale: Have each layer in your large app do a good job at abstracting away its implementation details. This way, callers of the save system need to learn about the saveGame
method, including the ways in which it communicates error conditions to you, but that's where the learning exercise ends. There is now no need for callers to know anything about DBs.
Conclusion: The AWS team is prejudicially arguing (they don't like checked exceptions for whatever reason and felt like writing down "We decided to change from checked exceptions to unchecked exceptions cuz we felt like it" sounded unprofessional so they reached out for the first plausible sounding reason without thinking about it very long), or seemingly inexperienced in how exceptions (checked or not) fit in proper design.
Remember: "Just make everything a runtime exception" is steering away from the type system, and most programmers agree that you should be using the type system more in order to keep larger projects maintainable. "Checked exceptions are bad for scalability" doesn't pass the smell test right off the bat, for that reason.
NB: This question is bordering on off topic, as SO isn't designed to tackle opinionated issues and checked exceptions have a rather large amount of opinionated surface area. However, there are somewhat objective issues, such as how to tackle 'hundreds of checked exceptions' and 'include the exceptions thrown by your implementation details in the job of ensuring implementation details are properly abstracted away' that are fit for SO.
NB2: I've attempted to think of every reasonable argument in favour of the axioms laid forth by the AWS team in the sections quoted by the question. I may well have failed to take into consideration a bunch of them. I'll attempt to update this answer if further reasoning is provided.