I guess exception handling is a more advanced concept of programming, and something not really taught at universities. Either way, I found that most developers (novices as well as experts) either do not understand exceptions, or they do not respect it.
I am not going to explain the details of what an exception is, for that I'll refer you to excellent books and articles on the subject. Rather, I will assume you have a basic working knowledge about exceptions and will try to explain the more practical issues here. Firstly, consider the following piece of Java code:
public int f(String pDatabaseName) {
int vCount = 0;
ConnectionManager vConMgr = ConnectionManager.getInstance();
Connection vCon = vConMgr.getConnection(pDatabaseName);
Statement vStmt = vCon.createStatement();
ResultSet vRes = vStmt.executeQuery("SELECT * FROM some_table");
while (vRes.next()) {
do_something();
vCount++;
}
vRes.close();
vStmt.close();
vConMgr.freeConnection(pDatabaseName,vCon);
vConMgr.release();
return vCount;
}
Depending on your level of expertise, you will either think nothing is wrong with this code, or that there is a
terrible flaw. Consider the normal flow of code. We start by retrieving an instance of the singleton ConnectionManager
.
Next, we use this instance to retrieve a Connection
object from the pool of cached connections. We
then create a Statement
, which we use to execute the SQL query on and lastly we step through the
ResultSet
. After we retrieved all the results, we close and release all the resources we allocated
in the beginning. If no abnormal situation occurs, this code will work perfectly. However, this is most
certainly not always the case. I can think of many possible scenarios - the database server might be down,
thereby throwing an exception in the vConMgr.getConnection(pDatabaseName)
call, the SQL query might
fail, thereby throwing an exception in the ResultSet vRes = vStmt.executeQuery("SELECT * FROM
some_table")
call, etc. I have seen many, many examples of a first try at fixing this looking
something like this:
public int f(String pDatabaseName) {
int vCount = 0;
ConnectionManager vConMgr = null;
Connection vCon = null;
try {
try {
vConMgr = ConnectionManager.getInstance();
vCon = vConMgr.getConnection(pDatabaseName);
Statement vStmt = vCon.createStatement();
ResultSet vRes = vStmt.executeQuery("SELECT * FROM some_table");
while (vRes.next()) {
do_something();
vCount++;
}
vRes.close();
vStmt.close();
}
finally {
vConMgr.freeConnection(pDatabaseName,vCon);
vConMgr.release();
}
}
catch (Exception e) {
}
return vCount;
}
This is actually worse code than the original version. What happens when the call to vConMgr =
ConnectionManager.getInstance()
fails? Assuming an exception is thrown, the finally
clause will
be executed. Note that initially vConMgr
was set to null, and when an exception is thrown in a method
(in this case ConnectionManager.getInstance()
, execution will resume in the innermost
try-finally
or try-catch
block - and no assignment will take place to
vConMgr
. Thus, in the finally
block, the first statement to be executed is vConMgr.freeConnection(pDatabaseName,vCon)
.
But vConMgr
is null
, hence the call is actually null.freeConnection(pDatabaseName,vCon)
- which is more than bad. You should never invoke a method on a null
object. This will
force a NullPointerException
to be thrown, and that will cause execution to resume in the try-catch
clause. In this exception handler, the exception object is not manipulated but silently suppressed.
Execution will now resume at the line return vCount
, returning the value 0. In itself this will not
really break anything, but the calling method will have no idea that this method failed to execute successfully,
there will be no way in which to determine why there was an error (because the exception is suppressed) even if you
knew it failed, and in general the real error would be hidden. Even if a stack trace printout was to be made in the
exception handler, the error would show as a java.lang.NullPointerException
, and not
the real reason - an exception in returning an instance pointer to the database connection manager. The reason is
obvious - the real error caused another error because it was not correctly handled, and it is always the last error
that will be detected, not the first one. These kind of errors are extremely difficult to trace because the real
error is hidden.
What happens when no connection object could be returned instead? An exception will be thrown by vCon =
vConMgr.getConnection(pDatabaseName)
, and by following the same reasoning as previously, the try-finally
clause will be executed causing the call to vConMgr.freeConnection(pDatabaseName,vCon)
to try and
free a null
connection, because vCon
is null. This will again suppress the original
exception and cause a phantom exception to be displayed if the appropriate code is added.
What will happen if an exception is thrown by the call to ResultSet vRes = vStmt.executeQuery("SELECT *
FROM some_table")
? Execution would jump to the try-finally
block, where everything would
work fine except of course the suppression of the exception in the empty try-catch
block. However,
if you take a closer look at the code, you will realize that the statement vStmt.close()
will not
be executed, thus we have a resource leak. In Java the garbage collector would reclaim the memory allocated to
the vStmt object, but the statement would not necessarily be closed properly (I know of many broken JDBC
implementations in which you need to explicitly close the statement). Given enough of these errors, and one day
you will find you are out of resources and thus the whole system will come tumbling down.
To illustrate the concept of Unexpected Exception Transformations, take a look at the following simple class:
import java.lang.*;
class secondary_exception {
public int f() throws Exception {
int vCount = 0;
String vStr = null;
try {
try {
if (1==1)
throw new Exception("This is the original exception");
vCount++;
}
finally {
vStr.indexOf("hi");
}
}
catch (Exception e) {
throw e;
}
return vCount;
}
public static void main(String[] args) {
secondary_exception vSE = new secondary_exception();
try {
vSE.f();
}
catch (Exception e) {
System.out.println("Exception: "+e.getMessage());
e.printStackTrace();
}
}
}
In the method f()
, we throw an exception to simulate a problem like a failed database connection,
network failure etc. This exception will be caught by the innermost try-finally
block, the line
vStr.indexOf("hi")
will be executed, then execution will jump to the innermost
try-catch
block and simply be re-thrown. In the main()
method, this exception will be
caught once again and simply displayed on screen. Here is a sample of the output:
waldo@waldonbl Play $ java secondary_exception
Exception: null
java.lang.NullPointerException
at secondary_exception.f(secondary_exception.java:17)
at secondary_exception.main(secondary_exception.java:31)
The e.getMessage()
returned the familiar useless message null, and the stack trace pointed to line
17 of secondary_exception.java
, which is the line vStr.indexOf("hi")
. The original
exception caused another exception because in the process of handling that exception, a bug in the code caused
another exception to occur and all information on the initial exception is lost. The reason is obvious - vStr
is a null
object and we can't call a method on a null
object. This is exactly the same
concept as that of the previous example, this time only with a simpler example. The big concern this causes is
that we can't find the original problem without having to fix a host of other bugs. In essence, this situation
could be prevented all together by a full understanding of how to use try-finally
and try-catch
blocks.
To revisit our first example, consider an improved version:
public int f(String pDatabaseName) throws Exception {
int vCount = 0;
ConnectionManager vConMgr = null;
Connection vCon = null;
try { // This try-catch handles any exception that occurs in the try-catch block
vConMgr = ConnectionManager.getInstance();
try { // This try-finally makes sure vConMgr.release() gets called
vCon = vConMgr.getConnection(pDatabaseName);
try { // This try-finally will make sure
// vConMgr.freeConnection(pDatabaseName,vCon) gets called
Statement vStmt = vCon.createStatement();
if (vStmt == null)
throw new Exception("Internal Error: vStmt is null in XX.f()");
try { // This try-finally will make sure vStmt.close() gets called
ResultSet vRes = vStmt.executeQuery("SELECT * FROM some_table");
try { // This try-finally will make sure vRes.close() gets called
while (vRes.next()) {
do_something();
vCount++;
}
}
finally {
vRes.close();
}
}
finally {
vStmt.close();
}
}
finally {
vConMgr.freeConnection(pDatabaseName,vCon);
}
}
finally {
vConMgr.release();
}
}
catch (Exception e) {
// Handle the exception
// In this case, we simply throw it out of the current method
// because we can't handle it. Normally we would not catch it at all
// if we can't handle it
throw e;
}
return vCount;
}
Can you see that there are now many more nested try-finally
blocks, because each one is only
able to protect one resource? Also, the try-finally
block
always starts just after the resource was allocated/created. Therefore, if the
allocation fails with an exception, the outer try
block will catch it and therefore no attempt will
be made to free the unallocated resource. Only if the allocation succeeded and the try-finally
block is entered, will any subsequent exceptions cause the resource to be released in the finally
clause. Also remember that all the finally
clauses will be called starting from the block the
exception occurred in, to the innermost try-catch
clause (which is usually after the outermost
try-finally
block). If however the allocation does not throw an exception on failure, but rather
returns a null
or similar "error" value, then that resource should be checked for a valid value and
an exception should be raised if not valid, just like it was done for the allocation of the vStmt
object. Note that the exception must be thrown on error before the try-finally
block is entered that protects that resource.
I have seen many examples of code where null
values are returned on error conditions, or other
"special" values indicating failure. There is in principle nothing wrong with this, however it is most of the time
not a very good method to use. On simple functions returning boolean values, it is okay to return say False on
invalid data. As an example, consider the following method:
bool isValidValue(int pValue) {
if (pValue > 0 && pValue <= 100)
return true;
else
return false;
}
It is clear that the best way to indicate an invalid value here is to return false. However, there are many, many other circumstances where it is much better to throw an exception on error conditions. An example follows:
boolean isValidMSISDN(String pValue) throws Exception {
if (pValue == null)
throw new Exception("Internal Error: pValue is null in method "+
"X.isValidMSISDN()");
if (doSomeChecks(pValue))
return true;
else
return false;
}
In this case, a null
value for a parameter is usually an indication of a programming bug made
somewhere else. If you want to test a number to see whether it is a valid MSISDN, it is logical to assume you
want to give it something to test. But when this code gets a null
value, it is usually a very good
indication of a lurking bug somewhere in the caller. If the code returned false
, then this possible
situation would not be detected. However, shouting like this on invalid data will only help find possible bugs
much more quickly. Note that there is a difference between invalid data and an invalid value. Invalid data refer
to a value that is not a valid instance of the representing data type, whereas an invalid value refers to a
valid instance of the representing data type, but an invalid value in the domain set for that value. In other
words, invalid data is exceptional (unexpected) conditions indicating a programming error, whereas invalid
values are expected user input errors which can be dealt with in the code.
I would like to explain a thing or two about catching and handling exceptions. Consider the following method:
void someMethod(String pValue) throws Exception {
try {
// Do things that might throw an Exception
...
}
catch (Exception e) {
throw e;
}
}
If you ever feel the need to write code looking like that, then you must realise you are doing something wrong. The rule is simple - never catch an exception you cannot handle. Why catch it just to re-throw it because you cannot handle the exception? In this case, the code could have been written better like this:
void someMethod(String pValue) throws Exception {
// Do things that might throw an Exception
...
}
Most developers I have met would very eagerly write code as shown below.
void someMethod(String pValue) throws DAOException {
try {
// Do things that might throw an Exception
...
}
catch (FileNotFoundException e) {
throw new DAOException("Error occurred in someMethod: "+e.getMessage());
}
}
Can you see what is wrong with this? In this scenario, the developer tried to wrap the exception in a more generic exception, which is considered good programming practise. Imagine if you always just throw the lowest level exceptions straight through to the front-end - you would have to handle many different exceptions. It is generally better to group them by wrapping them. In some instances you cannot get away from this, as is the case in RMI invocations in J2EE containers.
The problem with that code is that the stack trace has been lost. By throwing a new exception the original is
lost. Many times the only way to understand the root of the problem is to view the stack trace for the original
exception, which is now lost. Furthermore, imagine if the DAOException
is caught by a Session Bean,
and wrapped again in a RemoteMethodException
... Now you would only see the problem originating at
the catch
handler of the session bean!
The solution is very easy if you are using Java 1.4+, as Sun introduced the concept of exception chaining to
Throwable
. The aforementioned code can now be (better) written as follows, assuming DAOException
extends Exception
.
void someMethod(String pValue) throws DAOException {
try {
// Do things that might throw an Exception
...
}
catch (FileNotFoundException e) {
throw new DAOException("Error occurred in someMethod: "+e.getMessage(),e);
}
}
If you are using versions of Java prior to 1.4, you are out of luck. The only way to achieve this behaviour is to
define it yourself. An example has been provided, and you may use this in your code royalty free. Always extend
this class instead of Exception
if you want to chain exceptions.
public class NestedException extends Exception {
private Throwable mRootCause;
public NestedException(String pMessage) {
super(pMessage);
}
public NestedException(String pMessage, Throwable pRootCause) {
super(pMessage);
mRootCause = pRootCause;
}
public Throwable getRootCause() {
return mRootCause;
}
public String getMessage() {
return super.getMessage();
}
public void printStackTrace(PrintStream pPS) {
if (mRootCause == null)
super.printStackTrace(pPS);
else {
pPS.println(this);
mRootCause.printStackTrace(pPS);
}
}
public void printStackTrace(PrintWriter pPS) {
if (mRootCause == null)
super.printStackTrace(pPS);
else {
pPS.println(this);
mRootCause.printStackTrace(pPS);
}
}
public void printStackTrace() {
if (mRootCause == null)
super.printStackTrace();
else {
System.out.println(this);
mRootCause.printStackTrace();
}
}
}
This will only do half the work. The rest you need to do when displaying the exception, and the following code should help retrieving all the chained exceptions.
// Assume vE is the exception that needs to be displayed
ByteArrayOutputStream vB = new ByteArrayOutputStream();
PrintStream vPS = new PrintStream(vB);
vE.printStackTrace(vPS);
if (NestedException.class.isAssignableFrom(vE.getClass())) {
Throwable vRootCause = ((ServletException)vE).getRootCause();
while (vRootCause != null) {
vPS.println("Root Cause: "+vRootCause.getMessage());
vRootCause.printStackTrace(vPS);
if (NestedException.class.isAssignableFrom(vRootCause.getClass()))
vRootCause = ((NestedException)vRootCause).getRootCause();
else
vRootCause = null;
}
}
Using this in your code will ascertain that the whole stack trace be preserved, and displayed in full when needed.
I hope this article helped illustrate the proper way of using exception handling statements in creating more robust, error free programs. Just always remember to step through the code you just wrote manually by hand, and try to reason what would happen on exceptional and normal conditions. If the flow of the code makes sense, then it is a good indication that you are on the right track. However, if you see obvious problems, then you saved yourself hours of difficult debugging sessions. A proper self code review normally helps find 90% of all bugs in a system. Unfortunately many developers are too lazy to review their own code as they write it, and suffer the consequences of hours of endless debugging sessions.