Java Exception Rule Book

This post consists of a briefly outlined set of Java Exception rules, or best practices, with an accompanying look at rule compliance using specific transient network and database layer Exceptions.

The rules are based on the relevant rules from Joshua Bloch’s lauded and highly recommended Effective Java, 2nd Edition. Bloch is arguably the authority on the subject, and his legacy includes being listed as author of the Sun JDK Throwable implementation (see @author in the source code).

RULE BOOK

The author has violated these rules hundreds if not thousands of times over the last decade. As a mitigating factor it can be argued that the rules in themselves represent exception utopia, and so, it is highly unlikely that they will all be consistently followed in any given code base, or put differently, that even with the best of intentions any software engineer will follow all of them due to say project timeline pressures.

Such pressure may however be a symptom of not asserting one’s own standards when a unit of work or project commences, to alleviate pressure one can always clearly communicate one’s own non negotiable engineering standards at the start of a project.

Rule 1: Never use exceptions for ordinary control flow

import java.util.ArrayList;
import java.util.Iterator;

/**
* Do not, use Exceptions for flow control, this is an example of what not to do.
*
* @author nico
*/
public class DontUseExceptionsForFlowControl {

 public static void main(String[] args) {

   ArrayList list = new ArrayList();

   list.add("hello world");

   // Let's to the wrong thing, and jump out of this loop with an iterator related
   // Exception...

   Iterator iter = list.iterator();

   while(true) {

     try {

       System.out.println(iter.next());

     } catch (java.util.NoSuchElementException e) {

        // Using Exception for flow control, so can safely ignore this one.
        // TODO Stop using Exceptions for flow control.
     }

   }

  }

}

Using Exceptions for ordinary control flow violates the Principle of Least Astonishment which states “the result of performing some operation should be obvious, consistent, and predictable, based upon the name of the operation and other clues”.

Rule 2: Never write APIs that force others to use Exceptions for ordinary control flow

/**
* Database layer API, implementation detail agnostic.
*/
public interface EmailDatabaseHelper {

 // TODO refactor, this forces the user to catch checked exception EmailDoesNotExistException, with an email
 // address not existing being in no way exceptional, and hence in ordinary control flow
 public void doesEmailExist(String email) throws EmailDoesNotExistException;
}

Rule 3: Use runtime exceptions for programmer errors and checked exceptions where recovery is possible

Note that this rule does not state, as you may expect, that you must use unchecked exceptions for programmer errors, since that casts too wide a net. Unchecked throwables include runtime exceptions and errors, with the latter conventionally reserved for JVM error reporting under conditions where continued execution is impossible.

Rule 4: When uncertain as to whether a condition is recoverable or not, use an unchecked exception

The reasoning behind this is simple, if you do not know how an API user will recover from the checked exception, do not place the burden on the API user in terms of trying to figure out how, and potentially wasting time concluding that its not possible.

Rule 5: Only use checked exceptions if the API user can take action to recover from the said exception

If the only course of action, that you could take, when confronted with your own checked exception, is a variation of the below example then chances are good that you should not be using a checked exception. The checked exception adds no value, so, either attempt to refactor as per Rule 6, or use an unchecked exception.

} catch (CheckedExceptionWithNoUsefulActionPossible e) {
  logger.error("an error occurred");
  e.printStackTrace();
  System.exit(1); // or stopping the current thread
}

Rule 6: Refactor, where possible, checked exceptions that violate rule 5 into a state-checking method and unchecked exception

Before.

try {
  ball.kick(HARD);
} catch (BallCannotBeKickedException e) {
  // recover from exceptional conditions
}

After. State-checking method and unchecked exception has been introduced.

// ball will not be accessed concurrently, and so the calling sequence is safe
if (ball.isKickeable()) {
  ball.kick(HARD);
} else {
  // recover from exceptional condition
}

Rule 7: Clearly document if the unrecoverability of an unchecked exception is likely to be transient in nature

Certain conditions are unrecoverable at a particular instant but not permanently, and a prime example is an exception related to a lock being held on a database table on which say an update is being attempted. Methods that throw unchecked exceptions where the exception relates to a condition that may be transient in nature should document this fact, that is that the condition may be transient, and suggest a retry in the Javadoc comment associated with the unchecked exception.

Rule 8: Use the standard Java platform library unchecked exceptions where appropriate, do not re-invent the wheel

Reuse the standard Java platform library unchecked exceptions whereever possible, whilst honouring their documented semantics. A prime example is IllegalArgumentException. See the subclasses of RuntimeException for further candidates.

Rule 9: When dealing with lower-level exceptions inappropriate to the higher-level abstraction, perform either exception translation or exception chaining

Exception translation.

try {
     // lower level method invocation e.g. JPA call
} catch(LowerLevelMethodInvocationException e) {
     // now translate the lower level exception into an exception that matches
     // the higher level abstraction in our current context
     throw new HigherLevelException(...);
}

Exception chaining with chaining-aware contructor.

class HigherLevelAbstractionException extends Exception {
    HigherLevelAbstractionException(Throwable cause) {
         super(cause);
    }
}
try {
     // lower level method invocation e.g. JPA call
} catch(LowerLevelMethodInvocationException e) {
     // now translate the lower level exception into an exception that matches
     // the higher level abstraction in our current context
     throw new HigherLevelAbstractionException(e); // use chaining-aware constructor
}

Rule 10: You MUST document all exceptions, checked and unchecked, thrown by your methods

Excuse the all-caps and repetition will all exceptions being defined, but proper documentation is in no way negotiable and is every software engineers professional responsibility. The Javadoc @throws tag must be used to document both checked and unchecked exceptions in terms of the conditions under which they will be thrown. The only difference between checked and unchecked exceptions is that you must only define checked exceptions with the throws keyword, do not include unchecked exceptions here.

Rule 11: Force including the values of all parameters and fields that comprise the cause of an exception in its detail message

Support personnel or fellow programmers, when faced with a stack trace in say a log file, require pertinent data in order to ascertain exactly what caused an exception. Without the values of all parameters and fields that comprised the exception, it may be impossible to reproduce an exception.

In terms of forcing including pertinent parameter and field data in the detail message of exception, simply do not provide a constructor that has a string parameter as detail message, rather force the user to specify the said data in the constructor.

Consider the following checked exception, which has accessor methods since as per Rule 5 such exceptions should be used for recoverable exceptions.

/**
* @author Nico
*/
public class ServiceOperationException extends Exception {

private String suppliedApiKey;

/**
* Construct an ServiceOperationException.
*
* @param suppliedApiKey    the API key supplied to the service user upon successful registration
*/
public ServiceOperationException(String suppliedApiKey) {

// detail message
super("Supplied API Key: " + suppliedApiKey);

// capture for recovery purposes
this.suppliedApiKey = suppliedApiKey;
}

public String getSuppliedApiKey() {
return suppliedApiKey;
}

public void setSuppliedApiKey(String suppliedApiKey) {
this.suppliedApiKey = suppliedApiKey;
}
}

Rule 12: Ensure that your methods are failure atomic and if not document this fact in your API

Methods that are failure atomic leave an object in the state it was prior to the method invocation in the event of failure. Either ensure parameters have their validity checked before proceeding to to the actual work (and modification) in a method invoked on a mutable object, or ensure the relevant class is modified to be immutable.

Rule 13: Never ignore exceptions

From time to time you may see empty catch blocks, and in the worst case, catch blocks that catch java.lang.Exception (as listed in Tim McCune’s Exception-Handling Antipatterns) or java.lang.Throwable. This is a cardinal sin, and should never be done. At the very least, the exception should be logged (that is the entire stack trace should be logged) with an appropriate log level. In some cases, it may be justifiable to take no action, but in such cases, comments must justify why no action is taken.

RULE COMPLIANCE – RECOVERABLE TRANSIENT CONDITIONS

Some exceptions are associated with conditions that are possibly but not necessarily transient in nature, and so, when faced with them, the appropriate course of action is to automatically retry the operation. The Spring Batch Retry mechanism is geared for exactly such exceptions.

In terms of the rules, Rule 3, 4, 5, 6 and 7 are applicable, given our definition of the conditions being transient, but not with absolute certainty. In other words, given the lack of certainty as to whether the condition is recoverable, an unchecked exception should be used, as per Rule 3, 4 and Rule 5. If we choose not adhere to this rule, and feel the condition is indeed recoverable, and use a checked exception, as per Rule 5, then as per Rule 6 one should refactor into an unchecked exception with an accompanying state-checking method.

So, in short, we should use:

  • unchecked exception (if uncertain if recoverable) or if unrecoverability is a certainty
  • checked exception if recoverable
  • even better, unchecked exception with a state-checking method if recoverable

Database Layer

Consider the following exceptions, that one may see with a Spring / JPA / Hibernate stack:

  1. UnexpectedRollbackException (Spring)
  2. OptimisticLockException (JPA)
  3. LockAcquisitionException (Hibernate)

The decision to make all of these unchecked exceptions complies with the rules, given that within the context of a single transaction, the underlying condition is not recoverable. Within the context of retries, and so multiple transactions, the condition may indeed be recoverable due to the transient nature of the underlying conditions (a lock on a table will most likely not be permanently held). So, methods that throw these exceptions should firstly document that they throw these exceptions with @throws Javadoc (as per Rule 10) and then also document the fact that the user may wish to attempt retries (as per Rule 7).

Network Layer

Another example of an exception that may point to a transient condition is a SocketException, especially when considering its subclasses, BindException, ConnectException, NoRouteToHostException and PortUnreachableException. These exceptions are all checked exceptions, and this is incorrect since at the instant an associated method was called, recovery would not necessarily be possible, that is, recovery is not a certainty, so Rule 3 is violated. Rather, as per Rule 4, the exceptions should be unchecked and the transient nature of the exceptions should be documented as per Rule 7.

Key References

  1. Chapter 9 of Joshua Bloch’s (the author of java.lang.Throwable) Effective Java, Second Edition. If you don’t own a copy, buy one, you won’t regret it.
  2. Tim McCune’s Exception-Handling Antipatterns
About these ads

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s