*

Using Java Exceptions

Java supports throw/catch style exceptions. This can result in much cleaner, more robust, and easier to debug code if used correctly. This note provides some background information on Java exceptions and then a few simple rules for using exceptions effectively.

In Java, exceptions are raised by throwing an exception object that must be an instance of a class derived from java.lang.Throwable. Generally a new exception class adds no new methods or fields, but does provide two constructors, one that takes no arguments and one thats a single string argument (a message to associate with the exceptions). Figure 1, below, shows a typical exception definition. However, it is prefectly ok to add additional methods or fields to an exception class, such as an error code, or other context information. It would also be ok the add code to log exceptions in the exceptions constructors.

Figure 1: A typical exception class definition
--------------------------------------------------------
package mypackage;

public class MyException extends java.lang.RuntimeException {
  public MyException() {
    super();
  }
  public MyException(String message) {
    super(message);
  }
}
--------------------------------------------------------

Java provides a number of predefined exception classes, such as the class java.lang.RuntimeException used in Figure 1. When catching an exception the catch statement specifies the class of exception it will catch. Java will then direct any exception of that class or any sub-class of that class to the statements following the catch statement. (See Figure 2 below.)

Figure 2: Sketch of a try block
--------------------------------------------------------
  // ...
  try {

    // Some code that might raise MyException

  } catch (MyException e) {
    // This code will be executed only if a MyException
    // instance is thrown.
    // ... Code to handle the exception
  }
  // ... This will be executed after the try block even if a
  // MyException exception is raise unless an uncaught exception is
  // raised in either the "try" or "catch" parts of the block.
--------------------------------------------------------
Java requires that each method handle any exceptions raised in it unless the exceptions are listed as ones that can be thrown by the method, or unless the exceptions are derived from either java.lang.Error or java.lang.RuntimeException. This is intended to force a robust coding style where exceptions are always handled. (We will come back to this issue later.) A final comment about exceptions is that their typical implementation design point tries to reduce to zero the cost of potential exceptions at great increase to the cost of actually processing an exception. It is too early to have hard data on how high-speed Java implemenations will do this exactly, but experience with other languages indicates that the difference between an expection return and a normal return can be several orders of magnaitude. Simple measurements done with the Java JDK 1.02 interpreter shows only a factor of 2, but the same test run with the Cafe JIT shows a factor of about 100.

Guidelines

  1. Throw exceptions only when the calling code would not expect the result. That is, don't throw an exception for a normal result even when the result is fairly rare, but do throw an exception if the caller is not likely to want to test for the result. For example, if a search is always supposed to return something then throw an exception if it can't. However, if it is legal to specify a search that will not return anything then return null and let the caller test for null.

    Rationale: Exceptions are expensive to process. Also, It usually takes more code to catch an exception than it does to test for a result.

  2. Don't catch an exception unless you can handle it or you have to (to meet interface requirements). If you are only catching an exception to convert it to another because your interface requires it then print the stack trace when you catch the exception. Figure 3 shows one way to process an exception that is forced by interface requirements. If you can't print the stack trace because there is no where to print it to, then provide a log facility that you can use to log the real exception.
    Figure 3: Processing an exceptions that is required
              by an interface.
    --------------------------------------------------------
      void foo() {
        try {
          char c = System.in.read();
          // ...
        } catch (java.io.IOException e) {
          e.printStackTrace();
          throw new Error("Unexpected IOException");
        }
      }
    --------------------------------------------------------
    

    Rationale: Catching an exception that you don't handle masks the real exception and makes debugging harder. So if you must catch one at least print the stack trace so that the original exception point and type will be revealed.

  3. Declare all of your exceptions to be derived from either RuntimeException or Error. You should still declare them with a throws expression.

    Rationale:Exceptions derived from RuntimeException or Error do not have to be declared in interfaces which means that they can pass through the call stack until they hit a point where they can be really handled. This means that intermedite methods are not forced to catch the exception and then re-throw it as another exception. This reduces code bulk and give the best messages when debugging. Declaring them is still a good idea because it helps others to understand your interfaces.

  4. Suggestion: You may want to define two base exceptions, one for errors and one for runtime exceptions. These should be derived from java.lang.Error and java.lang.RuntimeException respectively. Then have all the exceptions that you define be derived from one of these two classes.

    Rationale: Gives a single point of change to introduce logging or other such support.