Fail Safe Exception Handling
Jakob Jenkov |
It is imperative that the exception handling code is fail safe. An important rule to remember is
The last exception thrown in a try-catch-finally block is the exception that will be propagated up the call stack. All earlier exceptions will disappear.
If an exception is thrown from inside a catch or finally block, this exception may hide the exception caught by that block. This is misleading when trying to determine the cause of the error.
Below is a classic example of non-fail-safe exception handling:
InputStream input = null; try{ input = new FileInputStream("myFile.txt"); //do something with the stream } catch(IOException e){ throw new WrapperException(e); } finally { try{ input.close(); } catch(IOException e){ throw new WrapperException(e); } }
If the FileInputStream constructor throws a FileNotFoundException, what do you think will happen?
First the catch block is executed. This block just rethrows the exception wrapped in a WrapperException.
Second the finally block will be executed which closes the input stream. However, since a FileNotFoundException was thrown by the FileInputStream constructor, the "input" reference will be null. The result will be a NullPointerException thrown from the finally block. The NullPointerException is not caught by the finally block's catch(IOException e) clause, so it is propagated up the call stack. The WrapperException thrown from the catch block will just disappear!
The correct way to handle this situation is course to check if references assigned inside the try block are null before invoking any methods on them. Here is how that looks:
InputStream input = null; try{ input = new FileInputStream("myFile.txt"); //do something with the stream } catch(IOException e){ //first catch block throw new WrapperException(e); } finally { try{ if(input != null) input.close(); } catch(IOException e){ //second catch block throw new WrapperException(e); } }
But even this exception handling has a problem. Let's pretend the file exists, so the "input" reference now points to a valid FileInputStream. Let's also pretend that an exception is thrown while processing the input stream. That exception is caught in the catch(IOException e) block. It is then wrapped and rethrown. Before the wrapped exception is propagated up the call stack, the finally clause is executed. If the input.close() call fails, and an IOException is thrown, then it is caught, wrapped and rethrown. However, when throwing the wrapped exception from the finally clause, the wrapped exception thrown from the first catch block is again forgotten. It disappears. Only the exception thrown from the second catch block is propagated up the call stack.
As you can see, fail safe exception handling is not always trivial. The InputStream processing example is not even the most complex example you can come across. Transactions in JDBC have somewhat more error possibilities. Exceptions can occur when trying to commit, then rollback, and finally when you try to close the connection. All of these possible exceptions should be handled by the exception handling code, so none of them make the first exception thrown disappear. One way to do so is to make sure that the last exception thrown contains all previously thrown exceptions. That way they are all available to the developer investigating the error cause. This is how our Java persistence API, Mr Persister, implements transaction exception handling.
By the way, the try-with-resources features in Java 7 makes it easier to implement fail safe exception handling.
Tweet | |
Jakob Jenkov |