Living with Exceptions

In Java, exceptions occur when some non-planned issue occurs that interrupts the normal flow of execution. Exception handling is accomplished in one of two general ways. The first is to simply allow the exception to propagate up the call stack. This is accomplished by adding the throws keyword and a list of exception types to the method declaration. The second approach is to execute code that may throw an exception inside of a try block. Immediately following the try block will be one or more catch blocks that catch the different exception types that may occur in the preceding try block. The catch block(s) may be followed by a finally block that is used for cleanup.

In the code sample below, an exception is raised at line 3 triggering a change in the expected flow of control in the program. Since the exception in some_method() was not in a try block, control transfers back toa_method(), which was invoked at line 8. In a normal (non-exceptional) situation the program would advance to line 9. Since the exception is in a try block, execution will transfer to the associated catch block to handle the exception. In the catch block the exception may be handled, ignored, or re-thrown. Note that at line 12 there is a return statement. If this code was not being executed as part of exception handling, control would return to the main() method. However, since this code is part of exception handling and there is a finally block, the code at line 15 will be executed before the return takes effect. If line 12 was a System.exit() invocation, the finally block would still be executed prior to the exit occurring.

 1 public class ExceptionHandling {
 2     public static void some_method() throws Exception {
 3         throw new Exception("Manually Thrown Exception");
 4     }
 5     public static void a_method(){
 6         try {
 7             System.out.println("Before Exception");
 8             some_method();
 9             System.out.println("After Exception");
10         } catch (Exception e) {
11             System.out.println("Handling Exception");
12             return;
13         }
14         finally{
15             System.out.println("Entered Finally");
16         }
17     }
18     public static void main(String[] args){
19         System.out.println("Calling Method");
20         a_method();
21         System.out.println("Returned From Method");   
22     }
23 }

Note in the output below that After Exception is never printed. Once an exception has occurred, the execution in the try block is done.

Calling Method
Before Exception
Handling Exception "Manually Thrown Exception"
Entered Finally
Returned From Method

A common pattern of execution is to handle the exception, and then continue execution after the try-catch-finally construct as shown below.

 1 public class ExceptionHandlingRecover {
 2     public static void some_method() throws Exception {
 3         throw new Exception("Manually Thrown Exception");
 4     }
 5     public static void a_method(){
 6         try {
 7             System.out.println("Before Exception");
 8             some_method();
 9             System.out.println("After Exception");
10         } catch (Exception e) {
11             System.out.println("Handling Exception");
12         }
13         finally{
14             System.out.println("Entered Finally");
15         }
16         System.out.println("Continuing After try-catch-finally");
17     }
18     public static void main(String[] args){
19         System.out.println("Calling Method");
20         a_method();
21         System.out.println("Returned From Method");   
22     }
23 }
Calling Method
Before Exception
Handling Exception
Entered Finally
Continuing After try-catch-finally
Returned From Method

This is potentially dangerous since the execution context may not be what you expect, especially if more than one exception is possible in try block.

Exception Handling Strategies

There are three basic strategies for managing exceptions. The first strategy is to ignore them, the second is to address them, and the third is to make them someone else's problem. Each strategy has some benefits and costs, and it is the responsibility of the developer to choose the correct strategy.

Ignoring Exceptions

Simply ignoring the exception by having a catch block that does not change program state is a tempting strategy that is unfortunately supported by many IDEs via code generation wizards. A common form of ignoring exceptions is shown below:

1 try {
2     some_exception_causing_method();
3 } catch (Exception e) {
4     e.printStackTrace();
5 }
6 finally{
7 }

Cost

Ignoring exceptions is tempting since you can simply continue along as if nothing happened. In the code sample a stack trace would be printed to the error console, but there is no guarantee that anyone would ever see it. An exception may be an indication that system state is such that the program cannot continue execution producing good results. Failure to appropriately address an exception may result in undefined behavior that is extremely difficult to diagnose and correct since the diagnostic information is only available in the error console. In some execution environments that output is not preserved, rendering it useless for debugging.

Benefit

There may be cases where an exception simply does not matter, and may be safely ignored. This is rare since exceptions are an indication that something is not working as expected. If an exception is ignored, it is crucial to document why the exception was ignored.

Caveats

A common pattern for reading files is to use an infinite loop to read the file, relying on catching an IOException when the end of file is reached to exit the loop. While this works, it is extremely poor practice. Reaching the end of a file is not the only reason an IOException could be thrown. Additionally, using the exception handling mechanism to break loops can make tracing code execution difficult.

Addressing Exceptions

For applications, addressing exceptions is the common strategy. Addressing an exception may take the form of recovering from the exceptional situation, or it may be alerting the user that there is a problem and terminating the program.

Costs

Addressing an exception by recovering may be challenging to the point of being impractical.

Benefits

If you are able to recover from an exception then the program does not need to terminate, thus leading to a better user experience. In the case where recovery is not possible, capturing the problem early and terminating may make debugging easier and also reduce the likelihood of data corruption and spurious program behavior.

Caveats

While it is tempting to recover from exceptions, this is often not advisable. Unless the execution context's validity is independent of the exception or can be repaired despite the exception, it may be the case that the program cannot correctly execute after an exception. Continuing execution is a good goal, but should only be attempted in cases when terminating the program is not an option or the exception does not lead to an invalid program state.

Making Exceptions Someone Else's Problem

For the developers of libraries and web services, this is often the correct strategy. An exception may arise because of invalid input provided to a library function. It is inappropriate for a library to attempt to correct an exception, but should instead indicate that an exception has occurred. In the case of web services, exceptions may arise as a consequence of corrupted web requests. A viable strategy for web services is to raise an exception to inform the web service container that there was a problem and let the container alert the client in an appropriate manner.

Costs

When throwing exceptions it is crucial that the exceptions be meaningful and provide diagnostic information back to the calling methods so they can deal with the exception correctly.

Benefits

By making the exceptions the problem of the caller, the code raising the exceptions can be simpler and focus on operating on correct inputs.

Caveats

As with any time you cause problems for somebody else, be sure that is an appropriate action. Additionally when you are writing an application, simply re-throwing exceptions will likely result in the application crashing. In Java (and other languages with exceptions) any exceptions that are not caught by the application will be caught by the runtime environment and mayresult in application termination without any cleanup.

Conclusions

Exception handling introduces a mechanism for altering the flow-of-control when issues arise during program execution. The proper response to exceptions depends on the context in which the exception occurs. Regardless of the context exceptions should be dealt with deliberately ensuring that they are not simply ignored.

Originally posted at Bespoke Bytes