Avoiding Java Exception Abuse
The capability to throw exceptions in Java gives a much-needed flexibility to the language. By being able to exit from the normal program flow, code can remain clear and easy to maintain. As is usual, with this added flexibility comes the temptation to abuse it. It is quite common to use exceptions as a way to exit out of a method during normal program flow. Although this style of programming is tempting, it is an abuse of exceptions that causes the code to be difficult to maintain and debug.
Return Early, Return Often
One of the most common abuses of exceptions is an attempt to avoid returning early. Edsger W. Dijkstra is often credited with claiming that methods should always have a single exit point. Although I disagree that Dijkstra claimed this, there has been a school of thought following the single exit point strategy. Trying to force a single exit point in a Java method often leads to clunky and unmaintainable code.
Programmers attempting to avoid layers and layers of nested code end up using exceptions to exit early in an attempt to avoid multiple exit points. Following this strategy, the programmer ends up with code that has to use a try/catch block instead of a simple conditional. Imagine a method that throws an exception instead of returning a false. The code that calls that method might look something like this:
try { chargeCustomerCard(variable1, variable2); updateDatabaseWithSuccessfulCharge(variable1, variable2); } catch (Exception e) { updateDatabaseWithFailedCharge(variable1, variable2); }
In this example, the catch block is used instead of a false result from the chargeCustomerCard() method. Of course, this begs the question, what happens if the chargeCustomerCard() throws a "real" exception? How is that handled? That might lead to further confusion:
try { chargeCustomerCard(variable1, variable2); updateDatabaseWithSuccessfulCharge(variable1, variable2); } catch (CreditCardException e) { logCreditCardException(variable1, variable2); } catch (Exception e) { updateDatabaseWithFailedCharge(variable1, variable2); }
As you can see, this quickly gets out of hand. Normal program flow is getting mixed up with exceptional situations. To avoid this, throw exceptions only for exceptional situations and use return codes or boolean values to control program flow:
try { if (chargeCustomerCard(variable1, variable2)) { updateDatabaseWithSuccessfulCharge(variable1, variable2); } else { updateDatabaseWithFailedCharge(variable1, variable2); } } catch (CreditCardException e) { logCreditCardException(variable1, variable2); }
This process not only produces code that is easier to read but it also allows unexpected exceptions to bubble up through the code to be either dumped out by the JVM or caught at a higher level.
Avoid putting yourself into this situation. If it makes sense to return from a method early, do so. Don’t throw an exception just to avoid multiple return points. In addition, check for known false results. If, in the above example, variable1 must be of a certain length, check the length—if it is wrong, return immediately. Returning early because of known bad situations will make the code easier to read and keep the proper path on the left margin of the method. This will be easier to maintain.