Sunday, November 25, 2012

Part VII - Effective Java - Exception Handling

This is the seventh post in the series of posts based on the book Effective Java by Joshua Bloch. In the previous post we read about General Programming practices to write good code in Java.

This section speaks about the best practices in handling Exception. This is one area that many do not understand properly and many of the problems arise due to lack of handling Exceptions.

Use Exceptions Only for Exceptional Conditions

Do not use exceptions to handle situations which can be handled without exceptions. E.g
try {
    while(true) {
        a[i++] .f();
    }
catch(ArrayIndexOutOfBoundsException e) {
}
Instead use
for (i = 0; i < a.length; i++) {
    a[i].f();
}
Exceptions as the name indicates should be used to handle Exceptional situations; they should never be used for ordinary control flow.

Use checked exceptions for recoverable conditions and run-time exceptions for programming errors

There are three types of exceptions in Java
1.       Checked Exceptions: These are exceptions that can be handled by the client. E.g. IOException, FileNotFoundException.  The compiler will force the user of the API to handle such exceptions. I.e. the user needs to either declare it in the methods throws clause or needs to put the code within a try – catch block where the Exception is handled.
2.       Run Time Exceptions: E.g. ArrayIndexOutOfBoundsException, NullPointerException. These are subclasses of java.lang.RuntimeException. The compiler will not force the user of the API to handle such exceptions.
3.       Errors: These are serious errors from which the users cannot be excepted to recover. OutOfMemoryError is an example of this. These are subclasses of java.lang.Error.
The Checked Exceptions should be mentioned in the throws clause so that the user of the API can handle it or pass it on to its caller to be handled by it.
Runtime exceptions should normally not be thrown or handled. But run-time can be used to indicate programming errors. E.g. one could throw an IllegalArgumentException if any of the parameters passed to the method does not meet the preconditions expected of it.
Errors should normally never be thrown or handled by any method.

Avoid unnecessary use of checked exceptions

 A method should throw as less a number of checked exceptions as possible. Throwing more checked exceptions makes it difficult for the user of the API to use the API and the user ends up handling a generic Exception. Throw checked exceptions if and only if the user of the API can recover from the exception that has been raised.
If some exceptions can be prevented by providing APIs for checking conditions that can throw the exceptions provide this function so that the user can use this API instead of handling a RunTimeException. E.g. if a method of a class can be invoked conditionally then provide a method by which the user can check if the method can be invoked before invoking the method so that we can throw a Run Time exception from the method if there is an issue.
E.g. instead of forcing the user to write a code as follows:
try {
    obj.action(args);
} catch(MethodAccessDeniedException e) {
    //Handle Exception
}
We could provide a method called has Access and allow the user to write  code as
if (obj.hasAccess(args)) {
    obj.action(args);
} else {
    //Handle no access situation
}
If a user invokes obj.action(args) without first checking for has Access then the user may get an RunTimeException.

Throw Exceptions Appropriate to the Abstraction

Sometimes if APIs throw Exceptions which are seemingly out of place it can be very confusing. This typically happens if one were to throw an Exception generated at the lower level directly to the upper level. To avoid this, higher layers should catch lower level exceptions and in their place throw exceptions that are explainable In terms of the higher-level abstraction. When doing this it is important to ensure that the lowest level of Exception and the stack trace associated with it is not lost. This can be achieved by using Exception chaining.
try {
    ….
} catch(LowerLevelException e) {
    throw new HigherLevelException(e);
}

Document all Exceptions thrown by each Method

Always document the exceptions that will be thrown by method. Never use a generic exception like java.lang.Exception or java.lang.Throwable to indicate the Exception thrown. This gives the user of API no idea of what actually is the Exception that is being thrown from the API.
1.       Document every checked Exception that the method can throw using the @throws tag and include them in the throws keyword in the method declaration.
2.       Document every unchecked Exception that the method can throw using the @throws tag, but do not declare it in the throws keyword along with the checked exceptions in the method declaration.
3.       If an exception is thrown by many methods in a class for the same reason then it is not wrong to document this at the class level indicating the reason for the methods throwing this exception.

Include failure-capture information in detail messages

When a program fails due to an unhandled exception the system automatically prints the stack trace which includes the detailed message in the Exception. This will typically be the only information somebody has to figure out what went wrong. So to make this comprehensive the message should contain the all the parameter values and field values that contribute to the exception. One way to ensure that the Exception has all the details required is to create a constructor which forces the user to provide the required information by default.

Strive for failure atomicity

Generally speaking, a failed method should leave the object in the state that it was in prior to the invocation. A method with this property is said to be failure atomic.
The ways to achieve this are
1.       Make the class immutable. This will ensure that the object is never altered irrespective of the exception thrown from any method
2.       Check the parameters and fields for validity before proceeding with the method execution. This will ensure the exception is thrown even before the object is mutated.
3.       Another option is to write a recovery code which can be invoked before an exception is throw to restore the object the state prior to the invocation of the method that is throwing the exception.
4.       The last option is to make a temporary copy of the object and work on it and if no exception occurs restore the new state to the original object. This can be an expensive operation and should be done if an only if really necessary.
This should be avoided if it makes the code more complex or if it drastically reduces the performance of the system.

Don’t ignore Exception

Of an exception is caught then it should be handled correctly. One should not have a code as follows:
try {
    …
} catch (SomeException e) {
}
An empty catch block defeats the purpose of exceptions which is to force the user to handle exceptional situations.
At the minimum have a comment explaining why the exception is being ignored.

In the next post we will read about best practices of using Threads in Java.

No comments: