Exceptions in Java: Nothing exceptional about them

Exception handling in Java from top to bottom

Java projects rarely feature a consistent and thorough exception-handling strategy. Often, developers add the mechanism as an afterthought or an as-you-go addition. Significant reengineering during the coding stage can make this oversight an expensive proposition indeed. A clear and detailed error- and exception-handling strategy pays off in the form of robust code, which in turn, enhances user value. Java's exception-handling mechanism offers the following benefits:

  • It separates the working/functional code from the error-handling code by way of try-catch clauses.
  • It allows a clean path for error propagation. If the called method encounters a situation it can't manage, it can throw an exception and let the calling method deal with it.
  • By enlisting the compiler to ensure that "exceptional" situations are anticipated and accounted for, it enforces powerful coding.

In order to develop a clear and consistent strategy for exception handling, examine these questions that continually plague Java developers:

  • Which exceptions should I use?
  • When should I use exceptions?
  • How do I best use exceptions?
  • What are the performance implications?

When trying to design APIs and applications that can cross system boundaries or be implemented by third parties, these issues only exacerbate.

Let's delve deeper into the various aspects of exceptions.

Which exceptions should I use?

Exceptions are of two types:

  1. Compiler-enforced exceptions, or checked exceptions
  2. Runtime exceptions, or unchecked exceptions

Compiler-enforced (checked) exceptions are instances of the Exception class or one of its subclasses -- excluding the RuntimeException branch. The compiler expects all checked exceptions to be appropriately handled. Checked exceptions must be declared in the throws clause of the method throwing them -- assuming, of course, they're not being caught within that same method. The calling method must take care of these exceptions by either catching or declaring them in its throws clause. Thus, making an exception checked forces the programmer to pay heed to the possibility of it being thrown. An example of a checked exception is java.io.IOException. As the name suggests, it throws whenever an input/output operation is abnormally terminated. Examine the following code:

try
{
   BufferedReader br = new BufferedReader(new FileReader("MyFile.txt"));
   String line = br.readLine();
}
catch(FileNotFoundException fnfe)
{
   System.out.println("File MyFile.txt not found.");
}
catch(IOException ioe)
{
   System.out.println("Unable to read from MyFile.txt");
}

The constructor of FileReader throws a FileNotFoundException -- a subclass of IOException -- if the said file is not found. Otherwise, if the file exists but for some reason the readLine() method can't read from it, FileReader throws an IOException.

Runtime (unchecked) exceptions are instances of the RuntimeException class or one of its subclasses. You need not declare unchecked exceptions in the throws clause of the throwing method. Also, the calling method doesn't have to handle them -- although it may. Unchecked exceptions usually throw only for problems arising in the Java Virtual Machine (VM) environment. As such, programmers should refrain from throwing these, as it is more convenient for the Java VM to manage this part.

java.lang.ArithmeticException is an example of an unchecked exception thrown when an exceptional arithmetic condition has occurred. For example, an integer "divide by zero" throws an instance of this class. The following code illustrates how to use an unchecked exception:

public static float fussyDivide(float dividend, float divisor) throws 
FussyDivideException
{
   float q;
   try
   {
    q = dividend/divisor;
   }
   catch(ArithmeticException ae)
   {
      throw new FussyDivideException("Can't divide by zero.");
   }
}
public class FussyDivideException extends Exception
{
   public FussyDivideException(String s)
   {
      super(s);
   }
}

fussyDivide() forces the calling method to ensure that it does not attempt to divide by zero. It does this by catching ArithmeticException -- an unchecked exception -- and then throwing FussyDivideException -- a checked exception.

To help you decide whether to make an exception checked or unchecked, follow this general guideline: If the exception signifies a situation that the calling method must deal with, then the exception should be checked, otherwise it may be unchecked.

When should I use exceptions?

The Java Language Specification states that "an exception will be thrown when semantic constraints are violated," which basically implies that an exception throws in situations that are ordinarily not possible or in the event of a gross violation of acceptable behavior. (See Resources for the The Java Language Specification, written by James Gosling, Bill Joy, and Guy Steele.)

In order to get a clearer understanding of the kinds of behavior that can be classified as "normal" or exceptional, take a look at some code examples.

Case 1

Passenger getPassenger()
{
   try
   {
      Passenger flier = object.searchPassengerFlightRecord("Jane Doe");
   catch(NoPassengerFoundException npfe)
   {
      //do something
   }
}

Case 2

Passenger getPassenger()
{
   Passenger flier = object.searchPassengerFlightRecord("Jane Doe");
      if(flier == null)
         //do something
}

In Case 1, if the search for the passenger is not fruitful, then the NoPassengerFoundException throws; whereas in Case 2, a simple null check does the trick. Developers encounter situations similar to the preceding in their day-to-day work; the trick is to engineer a sound and efficient strategy.

So, following the general philosophy behind exceptions, should you dismiss the possibility that searches will return nothing? When a search comes up empty, is it not more a case of normal processing? Therefore, in order to use exceptions judiciously, choose the approach in Case 2 over Case 1. (Yes -- We recognize the performance angle. If this code were in a tight loop, then multiple if evals would adversely affect performance. However, whether or not the if statement lies in the critical path would be known only after profiling and extensive performance analysis. Empirical results show that trying to code for performance up front -- ignoring sound design principles -- tends to produce more harm than good. So, go ahead and design the system right in the first cut, and then change later if you must.)

A good example of an exceptional situation: If somehow the object instance -- which the search method invokes -- was null, this becomes a fundamental violation of the getPassenger methods' semantics. In order to understand the performance implications of exceptions, read the paragraph on performance.

How do I best use exceptions?

All Java developers must address the challenging task of catching different kinds of exceptions and knowing what to do with them. This grows even more complicated when the code must transform the error messages from cryptic system-level exceptions to more user-friendly application-level ones. This holds true particularly for API-type coding, where you plug your code into another application, and you don't own the GUI.

Typically, there are three approaches to handling exceptions:

  1. Catch and handle all the exceptions.
  2. Declare exceptions in the throws clause of the method and let them pass through.
  3. Catch exceptions and map them into a custom exception class and re-throw.

Let's look at some issues with each of those options and try to develop a practicable solution.

Case 1

Passenger getPassenger()
{
   try
   {
      Passenger flier = object.searchPassengerFlightRecord("John Doe");
   }
   catch(MalformedURLException ume)
   {
      //do something
   }
      catch(SQLException sqle)
      {
         //do something
      }
}

At one extreme, you could catch all exceptions and then find a way to signal the calling method that something is wrong. This approach, as illustrated in Case 1, needs to return null values or other special values to the calling method to signal the error.

As a design strategy, this approach presents significant disadvantages. You lose all compile-time support, and the calling method must take care in testing all possible return values. Also, the normal code and error-handling code blend together, which leads to cluttering.

Case 2

Passenger getPassenger() throws MalformedURLException, SQLException
{
   Passenger flier = object.searchPassengerFlightRecord("John Doe");
}

Case 2 presents the other extreme. The getPassenger() method declares all exceptions thrown by the method it calls in its throws clause. Thus getPassenger(), though aware of the exceptional situations, chooses not to deal with them and passes them on to its calling method. In short, it acts as a pass-through for the exceptions thrown by the methods it calls. However this does not offer a viable solution, as all responsibility for error processing is "bubbled up" -- or moves up the hierarchy -- which can present significant problems particularly in cases where multiple system boundaries exist. Pretend, for example, that you are Sabre (the airline reservation system), and the searchPassengerFlightRecord() method is part of your API to the user of your system, Travelocity.com, for example. The Travelocity application, which includes getPassenger() as part of its system, would have to deal with every exception that your system throws. Also, the application may not be interested in whether the exception is a MalformedURLException or SQLException, as it only cares for something like "Search failed." Let us investigate further by examining Case 3.

Case 3

Passenger getPassenger() throws TravelException
{
   try
   {
      Passenger flier = object.searchPassengerFlightRecord("Gary Smith");
   }
   catch(MalformedURLException ume)
   {
      //do something
         throw new TravelException("Search Failed", ume);
   }
      catch(SQLException sqle)
      {
         //do something
         throw new TravelException("Search Failed", sqle);
      }
}

Case 3 meets midway between the two extremes of Case 1 and 2 by using a custom exception class called TravelException. This class features a special characteristic that understands the actual exception thrown as an argument and transforms a system-level message into a more relevant application-level one. Yet, you retain the flexibility of knowing what exactly caused the exception by having the original exception as part of the new exception object instance, which is handy for debugging purposes. This approach provides an elegant solution to designing APIs and applications that cross system boundaries.

The following code shows the TravelException class, which we used as our custom exception in Case 3. It takes two arguments. One is a message, which can be displayed on the error stream; the other is the real exception, which caused the custom exception to be thrown. This code shows how to package other information within a custom exception. The advantage of this packaging is that, if the calling method really wants to know the underlying cause of the TravelException, all it has to do is call getHiddenException(). This allows the calling method to decide whether it wants to deal with specific exceptions or stick with TravelException.

public class TravelException extends Exception
{
   private Exception hiddenException_;
   public TravelException(String error, Exception excp)
   {
      super(error);
      hiddenException_ = excp;
   }
public Exception getHiddenException()
{
   return(hiddenException_);
}
 }

What are the performance implications?

Exceptions come with a price, and in order to understand some of the issues involved, let's look at the mechanism for handling exceptions. The Java Virtual Machine maintains a method-invocation stack (or call stack) that features a list of the methods that have been invoked by a thread, starting with the first method the thread invoked and ending with the current method. A method-invocation stack illustrates the path of method invocations a thread made to arrive at the current method.

Figure 1. The Java method-invocation stack shows frames for the methods invoked.

Figure 1 shows a graphical representation of the method-invocation stack for our code. Inside the Java Virtual Machine, methods keep their state within the Java stack. Each method obtains a stack frame, which pushes onto the stack when the method invokes and pops from the stack when the method completes. (The stack frame stores the method's local variables, parameters, return value, and other key information needed by the Java VM). As methods continue to complete normally, their frames pop and the stack frame below turns into the currently executed method.

1 2 Page 1