Popular Posts

Sunday, March 05, 2006

Sun Tech Tip

BEST PRACTICES IN EXCEPTION HANDLING

Exception handling is a built-in aspect of the Java programming language. The concept of handling error cases with exceptions is designed to make code more reliable and easier to maintain and read. This tip looks at some best practices for dealing with and handling exceptions.

As a quick refresher, here's the full construct for exception handling:

   try {

// code which could potentially
// throw an exception

} catch (TheException1 e) {

// code to handle exceptional condition

} catch (TheException2 e) {

// code to handle next exceptional condition

} finally {

// code to run whether an exceptional
// condition happened or not

}

Basically, code that might throw an exception goes within a try block. A specific catch block code runs only if the exception identified for it occurs (or its subclass). However, the code in the finally block runs under all conditions -- this even includes the case when the try-catch code calls return to exit out of the current method.

The try block is required. Additionally, one of the two blocks, catch or finally, must be present, or the compiler will complain.

Given that you can have multiple catch clauses, the system finds the first one that matches the exceptional condition. So, if you have a catch block for an IOException, and a FileNotFoundException happens, this is caught by the general IOException catch clause.

This suggests a best practice for exception handling: Always work with as specific an exception as possible. For instance, if you create a method that could generate a FileNotFoundException, don't declare that method to throw an IOException. If you do, it forces the caller to handle all IOExceptions that are possible, not just FileNotFoundExceptions. You shouldn't get too specific though. In particular, the declared exception thrown by a method should never reveal implementation details.

Another best practice is try to avoid empty catch blocks. In other words, don't ignore exceptions when they happen.

   // This code is bad

try {
...
} catch (AnException e) {
}

The designers of system libraries throw exceptions for a reason: to tell you some exceptional condition happened. It's good practice to do something in response, even simply logging the problem. For classes you design, only declare methods to throw exceptions where you want the caller to deal with a problem. If you want to ignore an exception, perhaps because of an automatic retry that happens every minute or two, simply place a comment in the catch block to explicitly say why the exceptional condition is ignored.

   // Better
try {
...
} catch (AnException ignored) {

// System automatically retries every minute
// Nothing to really do until retry

}

One very important practice related to exceptions thrown from methods is documentation. There is a javadoc tag for documenting which exceptions are thrown by a method. It's @throws. Best practices call for all checked exceptions to be documented. Common runtime exceptions should be documented too. For example, in the following method declaration, both the thrown exception and why the throwable exception could be thrown are documented. Don't just have the @throws ExceptionName bit. That doesn't add any more than the method declaration alone.

    /**
* Loads the class
*
* @param name
* Class name
*
* @return Resulting Class object
*
* @throws ClassNotFoundException
* If class not found
*/
public Class loadClass(String name)
throws ClassNotFoundException {
...
}

For runtime exceptions, an exception here is the parseInt method of Integer. This can throw a NumberFormatException (this is declared as part of the method declaration). However, because this is a RuntimeException, it doesn't have to be declared.

Not only don't runtime exceptions have to be declared, but neither do they have to be checked. For instance, every array access can throw an ArrayIndexOutOfBoundsException. It is not good practice to wrap all array access code within a try-catch block. Doing that makes code difficult to read and maintain.

   // This is bad, don't do it:
try {
for (int i=0, n=args.length; i

For recoverable conditions, it is good practice to have catch clauses with actions involved. These are typically checked exceptions, although it's also possible with runtime exceptions. Case in point: the previously mentioned parseInt method of Integer. If you are validating user input with the parseInt method, that is certainly a recoverable operation (assuming the user is still available).

While it is possible to create your own custom exceptions, more typically, you can reuse the system exception classes. These are usually sufficient for most needs, though not for every need.

Runtime exceptions that you might use include the following:

  • NullPointerException - Tends to be encountered more by accident than by design. If this commonly happens to you, you might think of peer reviews.

  • IllegalArgumentException - Often encountered when validating arguments to a method. In some cases, they might result from the typical explicit checks at the start of a method. In other cases, they could happen after various operations are performed. Subclasses include NumberFormatException and PatternSyntaxException (for regular expressions).

  • IndexOutOfBoundsException - The ArrayIndexOutOfBoundsException class is a subclass, so is StringIndexOutOfBoundsException. You can use these in your classes and methods to check for passing in an integer beyond the end (or before the beginning) of your data structure.

  • UnsupportedOperationException - Typically used by the Collections Framework to indicate that a requested operation is not supported. You can use it to indicate similar usage problems.

Reusing existing exception classes allows developers to keep their codebase smaller and also take advantage of the familiarity they have with the existing exception class hierarchy.

The final aspect of exception handling to explore is exception chaining. Introduced with the 1.4 release of the Java 2 Platform, Standard Edition, exception chaining allows you to attach the underlying cause of a problem to the thrown exception. Chaining is not usually meant for an end user to see. Instead, it allows the person debugging a problem to see what caused the underlying problem.

For instance, in the database world, specifically in the JDBC libraries, it is common to have catch clauses for SQLException. Even prior to exception chaining, the SQLException clause had a getNextException method which allowed chaining of exceptions. To find out all the underlying causes of the SQLException, you could have a loop that looks something like the following:

   try {

// JDBC code

} catch (SQLException ex) {

while (ex != null) {
System.err.println(
"SQLState: " + ex.getSQLState());
System.err.println(
"Message: " + ex.getMessage());
System.err.println(
"Vendor: " + ex.getErrorCode());
System.err.println("-----");
ex = ex.getNextException();
}
}

Instead of using the JDBC version of chaining, the standard approach is now the one shown below. Instead of printing the SQL State, message, and error code, just the exception type and message are displayed.

   try {

// Normal code

} catch (AnException ex) {

Throwable t = ex;
while (t != null) {
System.err.println(
"Type: " + t.getClass().getName());
System.err.println(
"Message: " + t.getMessage());
System.err.println("-----");
t = t.getCause();
}
}

There is certainly much more that can be done with exceptions. Hopefully, the practices shown here should help get you started toward better understanding and usage.

For additional information on exception handling, see Chapter 8 "Exceptions" in "Effective Java Programming Language Guide" by Joshua Bloch.

No comments: