Effective Exception Handling in Java

  Peter Haggar is an Advisory Software Engineer with IBM in Research Triangle Park, NC. He can be contacted at [email protected].

EXCEPTION HANDLING IS a very powerful and useful feature of the Java language. With this power comes a degree of complexity that must be understood to effectively utilize exception handling in Java programs. Java introduces some new concepts to the exception handling model that make exceptions both easier and more difficult to use properly.

Exception handling is not the silver bullet it is sometimes made out to be. It will not solve all your error handling problems. In fact, it will introduce new problems, unless a well-rounded understanding of this technology is grasped. Too often with exception handling, programmers are led to believe that mastering a few new keywords will solve the error handling problems encountered with traditional error handling techniques. Exception handling simply represents a different avenue for dealing with errors. While exception handling is a step forward with regard to traditional error handling techniques, it creates new issues and problems that must be dealt with. Failure to understand these issues and understand the problems surrounding them can lead to the creation of additional problems in your error handling code.

When describing exception handling, most literature stops with a cursory look at the mechanics. What is truly needed to write solid Java code that utilizes exception handling properly is a full understanding of the mechanics of Java and then information on some of the nuances of Java exception handling. I examine the most interesting and least understood areas of Java exception handling: nuances and side effects. I also look at when to use it, when not to use it, how to use it effectively, and the impact of exception handling on the performance of your Java code.

THE NUANCES OF JAVA EXCEPTION HANDLING
All language features have their associated nuances. This is also true of exception handling in Java. Let's look at a few of them.

Keyword finally
The finally keyword is probably the best addition to the Java exception handling model over the C++ model. finally enables the execution of code to occur whether an exception occurred or not. Usage of finally is very good for maintaining the internal state of an object when an exception occurs as well as cleaning up non-object resources.

finally blocks are fairly straightforward, but have some behavior that can catch the unwitting developer off guard. finally blocks are always executed. More specifically, they will be entered when code within a try block:

  • throws an exception
  • finishes normally
  • executes a return statement
  • executes a break statement
  • executes a continue statement
Let's look at the ramifications of this. Listing 1 contains code that has some peculiar behavior.

Executing this code reveals two obvious results and one not so obvious. This code simply calls three methods, method1(), method2(), and method3(), and prints out the values they return. The call at //3 to method1() will result in 1 being printed. The call at //4 to method2() results in 2 being printed. The catch block in method2() is never executed because an exception is not thrown. However, the call to the method3() at //5 will print 4! This is because finally blocks are always executed regardless of what happens in a try block. In this case the value 3 is about to be returned at //1. However, on the execution of the return statement, control is transferred to the finally block at //2. This causes the return 4 statement to execute, resulting in method3() returning the integer 4.

Developers think that when they execute a return statement, they immediately leave the method they are executing. This is no longer true with the usage of finally. (finally is also entered on a break or continue statement.) This particular problem has the potential to lead to long debugging sessions if you are not aware of this side effect of finally.

The throws Clause
Many view the throws clause in Java as a savior. Remember the throws clause is where you have to list all of the checked exceptions that can propagate from a method. The compiler makes sure you either catch checked exceptions in a method or declare them in the throws clause of the method. The throws clause is provided to alert all callers of your method of the exceptions that it can generate. I think the throws clause is a useful language feature as implemented and provides useful information to users of methods. However, there is a side effect to it that needs to be considered as you are designing your Java code.

What happens when you are well into your coding of a fairly large project and you add a checked exception to a low-level worker method. For the purpose of this discussion, a worker method is one that performs a common task for many other methods in your system and is called from many, many places in your code. Typical systems have many such methods. You have two choices:

  • Catch the checked exception in this method and handle it there.
  • Throw the exception from the worker method to allow the caller to handle it.
Depending on the circumstances of your system, the first option may not be viable. This worker method may not have the means to resolve the exception itself. You may have no other choice but to choose the second option and throw this exception. This may not be as easy as it sounds.

Adding an exception to a throws clause of a method will effect every method that calls it. All methods that now call this worker method have to change. They also have the same two choices on what to do as listed above. If they decide to handle the exception, the exception will not propagate any further. However, what if these methods do not have the means of handling the exception? They too must rely on the second option and add the exception to their throws clause. After doing this you may attempt to compile your code again. When the compiler is done spitting out error messages, you may now realize what you are up against. Now, all methods that call all of the methods you just changed, have the same two choices. The process will continue all the way back to main(), assuming no intervening method handles the exception.

The moral of this story is not to add exception handling at the end of the development cycle. Your error handling strategy should be designed in from the beginning. If, as is often the case, you run into this situation, even with careful planning, you now know what you may be up against. The throws clause is a useful and beneficial language feature, but can also be painful if you're not careful.

Nested Exceptions
What happens during the processing of a catch or finally block, after an exception has been thrown, when some of the code contained within the catch or finally throws an exception? In addition, can you have a try/catch/finally block nested within a try, catch, or finally block? If so, what would be the flow through this type of code? Listing 2 contains a code example that answers these and other questions. Trace through the code and try to determine the output of the code (which is shown in Table 1).

Table 1. Output from code in Listing 2.
In main, calling nestingTest()
In outer most try block
In inner try block
In inner catch block
Exception caught: Exception thrown from inner try
In inner finally's try block
In inner finally's finally block
In outer catch
Exception caught: Exception thrown from inner finally's finally block
In outer finally
In main, caught exception: Exception thrown from inner finally's
finally block

The code in Listing 2 consists of various try/catch/finally blocks nested within one another. You can place try/catch/finally, try/catch, or try/finally blocks anywhere in your Java code. In addition, as is demonstrated, these blocks can be nested within each other. A try/catch/finally block can be placed inside of another try, catch, or finally block. There are ramifications to this, the least of which is the code can become somewhat confusing. The more serious side effect is that exceptions get lost or hidden from the caller.

Listing 2 consists of the method main(), which calls a method nestingTest() and catches an exception thrown from it at //1. Method nestingTest() consists of an outermost try/catch/finally block at //2, //12, and //13. Inside of the try block at //2, we have another try/catch/finally block at //3, //5, and //7. Inside of the finally block at //7, we have an additional try/finally block at //8 and //10. Let's examine the flow through this code.

On entering main(), we print a message and call method nestingTest(). Once inside nestingTest(), we enter the outermost try block at //2, print a message, and enter the inner try block at //3 printing another message. We then immediately throw an exception at //4. When an exception is thrown from a try block, control is transferred out of the try block to its corresponding catch block. If a catch block doesn't exist, control will transfer to the corresponding finally block. (Try blocks cannot exist without a corresponding catch or finally block.) In this case, control is transferred to the catch block at //5, and we print our message indicating the exception we caught. We then throw another exception at //6. Before we can leave this code, we must always execute the finally block. We enter the finally block at //7, and immediately enter yet another try block at //8. We print a message and throw another exception at //9. We then enter the finally block at //10, print a message, and throw yet another exception at //11.

So far, four exceptions have been thrown, one each at //4, //6, //9, and //11 and we have completed executing the outer try block at //2. It is interesting to note what happens next. We enter the catch block at //12, print a message indicating the exception we caught, then rethrow the exception and enter the outermost finally at //13 and print a message. When the finally block at //13 finishes, control is returned to the catch block in main() at //1. Note that the exception we catch at //1 is the exception thrown from //11. What happened to the exceptions thrown from //4, //6, and //9? The exception thrown at //4 was caught at //5 and not rethrown. The exceptions thrown from //6 and //9 were not caught by any code, but what has happened to them? In short, they are lost. They are also referred to as hidden. The exception thrown at //9 hid the one thrown at //6. The exception thrown at //11 hid both of them. What are the effects of hiding exceptions? For one, the original exception is lost and the code processing the last exception thrown (//12 in Listing 2) will not know about the previous exceptions thrown. This could be very bad, because the original problems reported in the exceptions thrown at //6 and //9 are not known.

Losing or otherwise hiding exceptions is not a good thing. What can or should you do about it? The best way to deal with this is when throwing exceptions from the inside of a try, catch, or finally block, wrap the existing exception inside of any new exceptions you may throw. This way, the receiver of the new exception will have information about all errors, and critical error information will not be lost.

The code in Listing 2 shows us that try/catch/finally blocks can be arbitrarily nested. In addition, only one exception can propagate from a try/catch/finally block even though many can be thrown from within it. Remember that only the last exception thrown will be seen by the caller while others will be hidden and lost.

WHEN TO USE AND WHEN NOT TO USE EXCEPTION HANDLING
Exception handling was devised as a robust replacement for traditional error handling techniques. Some believe that exception handling should be used for any and all error conditions and thus completely avoiding traditional error return code checking. I believe that exception handling can be overused. It should not be used exclusively or for control flow as a replacement for the if/else clause. I believe a more moderate approach is necessary whereby exception handling is used in conjunction with traditional error handling techniques.

Let's look at an example of this: Listing 3 contains some Java code that uses traditional error handling. Notice at //1 we are checking to see if the data value returned from the getData() method call is non-zero. If it is zero, we assume we are at the end of the stream. This makes sense and is intuitive.

Now let's change this code by taking a hard line on exception handling. We will not rely on return code values as is done at //1, but use exceptions. Listing 4 contains the code in Listing 3 with exception handling added.

Notice at //1 we are placing a try/catch block around the call to the getData() method. getData() will no longer return zero when the stream is empty, it will throw a NoMoreDataException. Even though the code in Listing 4 is a bit uglier than the code in Listing 3, some people believe that Listing 4 represents the way code should be written with exceptions and that returning error codes should never be done. I disagree. I think Listing 3 is much more intuitive even though it relies on the older methods of dealing with errors or unexpected results. I think exceptions can be overused and Listing 4 is a good example of it.

Exceptions should be used for conditions outside the expected behavior of the code. In the example above, we expect to get to the end of the stream, therefore a simple zero return from the getData() method is appropriate and intuitive. I don't think throwing an exception in this case is wise as this is not an exceptional condition but an expected one. We would not, however, expect that the stream is corrupted. That type of condition would warrant an exception being generated. The point is not to use exceptions for all conditions, but to use them where it makes sense, where exceptional conditions exist.

EXCEPTIONS AND PERFORMANCE
There are three areas of exceptions and performance to address. The first is the effect of throwing an exception. The second area concerns the effects of having try/catch blocks in your code even when no exceptions are thrown. The third area addresses the question: Can I write faster Java code by throwing an exception? The answer to this may surprise you.

Ramifications of Throwing Exceptions
As some may believe, the act of throwing an exception is not free. After all, what are Java exceptions? Java exceptions are objects and these objects need to be created. Creating objects is somewhat costly, therefore, the act of throwing an exception has some cost to it. To throw an exception in Java you write something like:

throw new MyException(...);
This creates a new object and then transfers control to one of multiple places. Because there is a cost to throwing exceptions, I recommend exceptions be used only for error conditions and not for control flow. See "Faster Code with Exceptions?" below for an exception (no pun intended) to this rule. The reason for this is due to the object creation aspect of exception handling, which thereby makes throwing exceptions inherently slow. When exceptions are used for error/failure conditions, I am not too concerned with speed. I want my code to run fast when it's working and normally don't care how long it takes to fail.

Another item to note is that the act of throwing an exception is analogous to a jump or goto statement. When an exception is thrown, program control is immediately transferred to one of three places:

  • catch block
  • finally block
  • calling method
All of these options have one thing in common. The code that was executing at the time an exception is thrown stops executing and control is transferred elsewhere. This has ramifications on the state and validity of your objects.

Whenever discussing performance and Java you have to account for the Java virtual machine (JVM) you are using and the operating system you are running on. I have seen large differences between JVMs I have used. Therefore, for what I am about to discuss, I highly recommend you perform some level of profiling on your systems first before assuming you will get the same results as indicated here.

Effects of try/catch Blocks on Performance
Placing try/catch blocks in your Java code can actually slow it down, even when exceptions are not thrown.

Listing 5 contains two methods, method1() and method2(), at //1 and //2, that contain almost identical code. Method1() does not contain any try/catch blocks, while method2() contains the code of method1() with the addition of a try/catch block in the body of the for loop at //3. method2() doesn't actually ever throw any exceptions, it just has a try/catch block surrounding code inside of its for loop. Running this code with JDK 1.1.4 on an unloaded Windows NT 4.0 machine produces results shown in Table 2. The average time indicated is based on 50 runs of the program with the specified array size.

Table 2. Average times for the code in Listing 5.
Array Size Average time for method1() Average time formethod2()
10,000 5 ms 6 ms
100,000 51 ms 59 ms
1,000,000 507 ms 588 ms

We notice that method2() is about 16% slower than method1() for large arrays. Because no exceptions are being thrown, this is simply a function of method2() having a try/catch block in its version of the for loop. If the try/catch block is placed outside of the for loop, there is no difference in the execution time of the two methods.

In general, it is a better programming practice to place try/catch blocks outside of loops. Failure to do this can have a negative impact on the runtime performance of your code. Having said this, it is true that you will not see slower results on some JVMs. Running this code on JDK 1.2 does not show any difference in the timings. However, you may not know which JVMs your customers will be running, so it is still a good idea not to place try/catch blocks inside of loops.

Faster Code With Exceptions?
After what we have seen so far, you may not think it's possible to write Java code that utilizes exceptions that is faster than Java code that does not. There are actually cases where this is possible. Let's consider the code in Listing 6.

Listing 6 contains two methods, method1() and method2(), at //1 and //3, that contain almost identical code. Method1() at //1 creates an array of integers, then initializes each value in the array from 0 to the size of the array minus 1. Notice at //2, the loop to perform the assignments is a typical for loop in Java. It starts at 0, increments by 1, and terminates when the counter becomes equal to the size of the array. The time it takes to complete this operation, in milliseconds, is then calculated and returned.

Let's compare this to method2() at //3. Method2() performs the exact same operation on an array, but the for loop is different. Notice at //4 the for loop is coded to run indefinitely. It still starts at the value 0 and increments by 1, but it does not check when the counter becomes equal to the size of the array. Instead, this code waits for the ArrayIndexOutOfBoundsException to terminate the loop at //5. The time it takes to complete this operation, in milliseconds, is then calculated and returned.

Running this code with JDK 1.1.4 on an unloaded Windows NT 4.0 machine produces the results shown in Table 3. The average time indicated is based on 50 runs of the program with the specified array size.

Table 3. Average times for the code in Listing 6.
Array Size Average time for method1() Average time for method2()
10,000 5 ms 4 ms
100,000 50 ms 42 ms
1,000,000 497 ms 410 ms

Table 3 indicates that the time to execute the loop in Listing 6 at //4 is about 17% faster than the loop at //2 for large arrays. What is going on here? We just discussed how throwing exceptions is expensive and having try/catch blocks in your code can slow it down. Given this, how can the code in method2() in Listing 6 take less time to execute than the code in method1()?

The main reason is because the for loop in method1() at //2 performs two checks each time through to see if it has reached the end. The first check is done with the i statement of the for loop. The second check is performed by the Java runtime to see if the array access is out of bounds or not. The for loop in method2() at //4 eliminates the first check and relies on the Java runtime to perform the array bounds check. The Java runtime will throw the ArrayIndexOutOfBoundsException if you attempt to access beyond the bounds of an array. Therefore, the loop in method1() performs two checks: one in the loop and the other in the Java runtime. The code in method2() only performs one check in the Java runtime because its for loop does not check an end condition. When the loop has reached beyond the end of the array, the Java runtime throws the ArrayIndexOutOfBoundsException. This exception is caught at //5 and the loop is terminated. Also note that the try/catch block is placed outside of the for loop. This is crucial for the performance gain.

Note, regarding the technique used in method2() of Listing 6—I do not recommend using this technique casually. It can be confusing and is not a standard way to program. Using it only when you have a very performance-critical area of code. In addition, not all JVMs exhibit the same timing behavior. I ran the same code on JDK 1.2 and saw no difference in the timings. This is due to the different optimizations used in JVMs. Therefore, I highly recommend before using this technique that you profile carefully to determine that this actually results in faster code on the JVM you are going to run on. Also, if you use this trick, comment it well for the person who may modify the code at a later time.

EFFECTIVE EXCEPTION HANDLING
All of the above information is useful, but I don't want to lose sight of what you are trying to accomplish when an exception is thrown. Throwing an exception is easy. The hard part is minimizing the damage you can cause by throwing one.

Object State Management
What good is it to throw an exception if you leave an object in an invalid or undefined state? After all, the code catching the exception you throw may handle it and recover and call your code again. If your object was left in a bad state, there is a chance that the code will fail. This situation can be more difficult to debug than if you just terminated the program on the first exception instead of trying to recover from it. When throwing exceptions, it is important to consider what state your object is currently in. If it's in an invalid state, consider what has to be done to place it in a valid state before throwing the exception. Consider the code in Listing 7.

Listing 7 contains an add() method, which adds an object to some list. The first thing it does is increment a counter at //1 for the number of objects in the list. It then conditionally reallocates the list and adds the object to the list at //2. The code in Listing 7 is flawed. Notice that the code between //1 and the addToList() call at //2 can throw exceptions. If an exception is thrown after //1, the object is now in an invalid state because the counter, numElements, is incorrect. If the caller of this method recovered from the thrown exception and called this method again, there is a good chance that other problems will arise because the object state is invalid.

The fix for this problem is a simple one and is shown in Listing 8. We simply move the increment of numElements to the end of the method at //1. This ensures the counter, numElements, is accurate because we increment it after we have successfully added the object to the list. This is a simple example, but it exposes a potentially serious problem. You need not only worry about the object you are currently running in, but other objects you may have modified in your method. You may have completely or partially created a file, opened a socket, or made remote method calls to another machine when you threw an exception and exited the method. You need to be aware of the object states that exist on your local machine, or a remote machine, if you expect your code to work properly when reentered at a later time.

If you expect the callers of your code to recover from the exceptions you generate, you should review all of the places where you generate exceptions to see if you are leaving your objects in a valid state. If you are not, you need to ensure that if your code is entered again, it will work properly. The finally keyword can and should be used to help preserve object state.

Unfortunately, this is extremely difficult to do properly. In your method, you may have called many other methods that have changed many states of objects. If you throw an exception and leave your method, you will have quite a bit of work to clean up to get back to a state where your code is valid again. This problem is not easily solved and requires much thought and effort. It is the single biggest inhibitor in producing really robust Java code that utilizes exceptions properly.

SUMMARY
Exception handling, although useful, is not a cure-all for your error handling problems. There are many areas of exception handling that are not very obvious and can lead to more complexity and more problems. Used properly and carefully, exception handling has many benefits. It has its share of pitfalls, and these can and should be avoided through judicious use of these concepts.

Quantity reprints of this article can be purchased by phone: 717.560.2001, ext.39 or by email: [email protected].