Chapter 85. Uncheck Your Exceptions

Kevlin Henney

If you ever want to walk to hell, the journey will be easy on your feet. The whole road is very well paved, with good intentions as far as the eye can see. At least one of those paving stones is dedicated to Java’s checked exception model.

A checked exception is one that, if not handled within a method, must appear in the method’s throws clause. Any class descended from Throwable can be listed after throws, but unhandled checked exceptions (not descended from either RuntimeException or Error) must appear. This is a feature of the Java language, but it has no meaning for the JVM and is not a requirement for JVM languages.

The good intention here promotes a method’s failures to the same type-level significance as its success-scenario inputs and outputs. At first sight, this seems reasonable. Indeed, in a small and closed codebase, this type-level confidence that some exceptions are not overlooked is an easy goal to meet and, once met, offers some (very) basic reassurance about the completeness of the code.

Practices that might work in the small, however, are under no obligation to scale. Java’s checked exceptions were an experiment in combining control flow with types, and experiments produce results. The designers of C# learned from the experience:

C# neither requires nor allows such exception specifications. Examination of small programs leads to the conclusion that requiring exception specifications could both enhance developer productivity and enhance code quality, but experience with large software projects suggests a different result—decreased productivity and little or no increase in code quality.

The designers of C#, of other JVM languages, of other non-JVM languages…whatever the original intent, the day-to-day reality of checked exceptions is they’re perceived as obstacles. And if there’s one thing programmers are skilled at, it’s working around obstacles.

Compiler complaining about an unhandled checked exception? One IDE shortcut later, the obstacle is gone! In its place, you have an ever-lengthening throws clause that pushes incidental information into published signatures, often leaking details that should be encapsulated.

Or perhaps you add throws Exception or throws OurCompanyException to every method, noisily defeating the goal of being specific about failure?

How about catch-and-kill? If you’re in a rush to push your code, there’s nothing an empty catch block can’t fix! You are Gandalf to the checked exception’s Balrog—“You shall not pass!”

Checked exceptions bring and inspire syntactic baggage. But the issues run deeper. This is not simply a matter of programmer discipline or tolerating verbosity: for frameworks and extensible code, checked exceptions are flawed from the outset.

When publishing an interface, you’re committing to a contract signed with method signatures. As Tolstoy recognized in Anna Karenina, the rainy-day scenarios are not as simple, as certain, or as knowable up front as the happy-day scenarios:

All happy families are alike; each unhappy family is unhappy in its own way.

Interface stability is hard. Interface evolution is hard. Adding throws makes everything harder.

If someone plugs code into yours, and uses your code in their application, they know what they might be throwing, but you neither know nor care. Your code should let exceptions pass from their plugged-in code through to the handlers in their main application code. Open inversion of control demands exception transparency. 

If they’re using checked exceptions, however, they can’t use your interfaces unless you add throws Exception to every method—noise that creates a burden on all dependent code—or unless they tunnel their exceptions wrapped in a RuntimeException…or unless they change their approach, standardizing on unchecked exceptions instead.

This last option is the lightest, most stable, and most open approach of all.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.59.187