Skip to main content

Java Exception Handling Patterns (Part 1)

Posted by bakksjo on September 19, 2005 at 9:22 AM EDT
In a series of blog entries, I intend to discuss various patterns for exception handling in Java.

Don't use enumerated exceptions

Quite often, you will see code using what I like to call "enumerated exceptions". An enumerated exception typically looks like this:

public class ModuleException
   extends Exception
{
   private int exceptionNumber;

   public final static int EXCEPTION_A = 1;
   public final static int EXCEPTION_B = 2;
   public final static int EXCEPTION_C = 3;

   public ModuleException(int errcode)
   {
      exceptionNumber = errcode;
   }

   public int getErrorCode()
   {
      return exceptionNumber;
   }
}

Problem 1: Over-declaring exceptions

Consider a method which is declared like this:

public void myMethod
   throws
      ModuleException
{
   ...
}

To someone who does not completely know the internals of myMethod, there is no way to safely interpret this declaration other than as the logical equivalent of this:

public void myMethod()
   throws
      EXCEPTION_A,
      EXCEPTION_B,
      EXCEPTION_C
{
   ...
}

As such, code which calls myMethod must, in principle, check for all these exceptions, like this:

try {
   myMethod()
} catch (ModuleException e) {
   switch (e.getErrorCode()) {
   case ModuleException.EXCEPTION_A: ...;
   case ModuleException.EXCEPTION_B: ...;
   case ModuleException.EXCEPTION_C: ...;
   }
}

This is clearly cumbersome and wasted if myMethod only throws ModuleExceptions with errorcode A, but there is no way to declare that it does not throw type B or C.

However, nobody ever writes such exception handlers, for a number of reasons:

Applicability: More often than not, there is no logical or natural way to handle some of these exceptions in the current context, since exceptions A, B and C may be of completely different nature and occur in distinct places.

Volume of single catch-handler: Enumerated exceptions often contain hundreds of possible values, which makes it incomprehensible to write a handler for each one after every call of every method which declares that it throws an enumerated exception.

Number of handlers to write: In modules which use enumerated exceptions, it is often practice that almost every method in the module declares that it throws the enumerated exception, and usually only this one. As a consequence, almost each method called from client code will possibly have to catch the enumerated exception, so using the principle outlined above would turn the majority of the client code into exception handling. Even worse, most of the exception handling code would, in practice, be produced by copy-and-paste, and often incorrect in the current situation. Nothing would be gained except making the code completely unreadable.

Maintainability: Whenever somebody adds EXCEPTION_D to ModuleException, each existing exception handling block should, in principle, be edited to include a handler for the new exception.

For these reasons, catch handlers for enumerated exceptions hardly ever test for all possible error codes; instead, people write their exception handlers to handle a subset of the error codes in ModuleException: the error codes they "know" that the method uses. This brings us over to the next problem:

Problem 2: Disabling compiler checking of exception handling completeness

A programmer saying that she knows which error codes in an enumerated exception are relevant after a certain method call is like saying that she knows what exceptions a method throws if the Java language did not require you to declare them and the compiler did not check that they were handled.

Most Java programmers have experienced the following scenario: You write a new method or modify an existing one. You call methods in various libraries; say, e.g., container-related methods from the java.util package. You read the javadoc for this library to make sure you understand how the methods work, what input parameters they take, what their return values are. You think your modifications are complete, so you compile and test your program - and then you find that the compiler complains to you that you have uncaught exceptions; you need to either handle a certain exception or declare that your method throws it.

The exception was written in the documentation, but you missed it and did not write a handler for it anyway. Not until the compiler told you that you needed to do so. Only then you sat down and asked yourself: "What should my program do when that exception is thrown?"

Using enumerated exceptions effectively disables compiler checking of the completeness of your exception handling. This is extremely dangerous and nonchalant! Consider the following code:

public void methodA()
   throws
      ModuleException
{
   ...
   throw new ModuleException(ModuleException.EXCEPTION_C);
   ...
}

public void methodB()
{
   try {
      methodA()
   } catch (ModuleException e) {
      switch (e.getErrorCode()) {
      case ModuleException.EXCEPTION_B: doSomething();
      }
   }
}

Here, the programmer of methodB thinks that methodA only throws EXCEPTION_B. There is no handler for EXCEPTION_C, which is actually thrown (maybe it was added after was written), and the result may be disastrous, but the compiler will be happy.

Consider instead:

public void methodA()
   throws
      ExceptionB,
      ExceptionC
{
   ...
   throw new ExceptionC();
   ...
}

public void methodB()
{
   try {
      methodA()
   } catch (ExceptionB e) {
      doSomething();
   }
}

The compiler will complain to you, since ExceptionC is not caught and not declared to be thrown in methodB.

Observe that in the first example, declaring that methodB throws ModuleException does not help - EXCEPTION_C will not be passed on to the caller of methodB anyway. In order to pass it on, it has to be explicitly tested for in the switch block and re-thrown, whereas in the last example, one can add ExceptionC to the throws clause of methodB and force the caller of methodB to handle it.

Problem 3: Polluting the exception-class with non-relevant fields and methods

Often, one will want to add fields or methods to some exception types. With distinct classes for each exception type, this is no problem. With enumerated exceptions, it usually means adding methods and fields which are relevant for some error codes, and have no meaning for others. Quite often these classes end up somewhere in the middle - they try to restrict the level of pollution, so they don't go "all the way" in providing all possible information that could be supplied with single error codes. This causes them to be neither "pure" and general, nor as specific as they should be for a particular case. This should be a clear indication to the programmer that enumerated exceptions are a bad idiom, and one should rather use multiple, separate exception classes.

You will see numerous modules using this pattern, such as, for instance, JDBC:

try {
   // execute SQL statement
} catch (SQLException e) {
   if (e.getSQLState().equals("40001")) {
      // Terminated because of deadlock
   }
}

If the exception above was instead a separate class (say, SQLDeadlockException), it could contain more information, e.g. what locks the statement could not contain.

The fact that the pattern is commonly used in "big" libraries/APIs does not make it a better pattern. All drawbacks listed above apply to all the modules which use this pattern. In JDBC, for instance, developers typically "forget" to write code to handle retryable exceptions. The compiler forces them to catch a SQLException, but since they have little or no idea up-front of what kind of errors this exception may be, they don't write the if ()-test in the catch handler and simply "give up" executing the statement.

By now I should have convinced you not to use enumerated exceptions, right? ;o) So, what should you do then? General rule: Write separate classes for all your exceptional conditions. Declare for each method exactly which of these are thrown. Do not declare to throw some big, fat, one-exception-to-rule-them-all.

Stay tuned for part 2: Objections to this rule, adaptations you can make in certain cases where this simple advice may not suffice.
Related Topics >>