Java Exceptions

This page describes how to use exceptions. (Some discussion about communicating exceptions over JMS is at OmExceptionsIBatchJobs ).

Background

Exceptions were designed to simplify error handling. Without exceptions (as in C) code often looked like this:

... if (ISOK != doSomeThing(i, p1) ) {

 . // cleanup if (null != p1) free(p1); return ERROR;
}

if (ISOK != doSomeThingElse(k, p1, p2) ) {

 . // cleanup if (null != p1) free(p1); if (null != p2) free(p2); return ERROR;
} ...

The introduction of exceptions and automatic memory management makes it possible to replace the code above with:

... doSomeThing(i, object1); doSomeThingElse(k, object1, object2); ...

This code is clearly much easier to read and maintain.

The objective of guidelines for exception handling should be:

•Ensure that errors are handled appropriately

•Result in easy to read code

•Minimize the code needed to deal with errors

Catching exceptions

The general rule is: Avoid catching exceptions unless you need to catch it. Also expressed as "Never catch an exception that you do not know how to handle" (with apologies to H. P. Lovecraft). You need to catch an exception if:

•Resources must be released. Pay special attention to constructors. Consider whether you can handle it with the finally construct

•Your code can fix the problem and try again

•Your code must try an alternative execution strategy

•Your are implementing a toplevel method like main() Consider the following example of how NOT to handle exceptions

... public void myMethod(A a, B b) {

 . try {
  . processA(a);
 } catch (ExceptionA e) {
  . System.out.println("Ups - " + e); // WRONG! Easily missed and breaks assumptions of processB
 } try {
  . processB(b);
 } catch (ExceptionB e) {
  . System.out.println("Ups - " + e); // WRONG! Easily missed
 }
}

... public void myMethod(A a, B b) {

 . try {
  . processA(a); processB(b);
 } catch (ExceptionB e) {
  . System.out.println("Ups - " + e); // WRONG! Easy to miss
 } catch (ExceptionA e) {
  . System.out.println("Ups - " + e); // WRONG! Easy to miss
 }
}

This code looks better but it still has problems. The catch blocks don't do much and the method has no way to inform its users that the processing failed. A better implementation would thus be:

... public void myMethod(A a, B b) throws ExceptionA, ExceptionB {

 . processA(a); processB(b);
}

In general, do not catch an exception merely to log or print something. It clutters the code and the output and increases the risk that the exceptions is accidentally lost. However, see one exception in the JavaLogging pages.

Exceptions that disappear before their time have caused some of our most insidious bugs. It becomes both harder to notice the problem and harder to debug it. Remember also that exceptions carry valuable information about problems in the execution. Such information should not be lightly thrown away.

Use of finally

Special care is needed when a finally block is used. If an exception is thrown in the try block, and a new exception is thrown in the finally block, information about the first exception is effectively lost. This may lead to hard to find bugs. A finally block should therefore always be accompanied by a catch throwable that ensures that all exceptions are logged.

try {

 . doSomething();
} catch (Throwable t) {

 . // make sure the exception is logged, even when finally throws a new exception log.log(Level.WARNING, "doSomething failed", e); // Remember to rethrow throw t;
} finally {

 . // resources should always be released releaseIOresources();
}

Releasing resources

Your code must always expect that any statement might throw an exception. Any resources that are allocated and need to be released (file handles, database connections, JMS connections,...) be released in a catch or finally block. Notice however that this does not necessarily need to happen within the same method, it is often better to provide an appropriate method and let the client code invoke this method.

class AConnection {

 . /** contains a piped connection
  * /
 private Pipe p = null; /** Constructor that creates an object that is ready to establish a connection
  * @see doConnect
  * /
 public AConnection() { } /** Establish a connection to the supplied destination.
  * @throws ArgumentNotValid if destinationId is "" or null
  * @throws IOFailure if no connection could be established
  * @throws UnknownId if the destinationId is unknown
  * /
 public doConnect(String destinationId) {
  . if ((destinationId == null) || (destinationId.equals("")) {
   . throw new ArgumentNotValid("Destination id=" + destinationId);
  } p = makePipeConnection(destinationId);
 } /** Release resources allocated to establish a connection.
  * Must always be invoked as the last action on AConnection to prevent
  * resource leakage
  * /
 public void close() {
  . // verify that a connection has been established and close it if (p != null) {
   . p.close(); // notice the chaining of close operations here
  }
 }
}

.... // client code void foo() {

 . AConnection c = new AConnection(); try {
  . String connectionName = Config.getDestinationId(); c.doConnect(connectionName); doSomeOtherProcessing(c);
 } catch (Throwable e) {
  . log.log(Level.WARNING, "Connecting and processing failed", t); // rethrow throw e;
 } finally {
  . c.close();
 }
}

In the example above the resource was allocated outside the constructor. If resources are allocated inside the constructor you can not rely on close() being invoked if the constructor throws an exception. It is therefore necessary explicitly to release already allocated resources in the constructor if an exception occurs.

Throwing Exceptions

The following exceptions defined in dk.netarchive.exceptions may be thrown explicitly:

Exception Usage Examples Extends NetarkivetException Never throw, only use to catch catch (NetarkivetException e) RuntimeException ArgumentNotValid One or more arguments are invalid Null argument, empty string or integer out of range NetarkivetException IOFailure An input/output operation failed. reading from file, JMS connection terminated NetarkivetException NotImplementedException A method was called that is not yet implemented. NetarkivetException PermissionDenied Access was denied to a resource or credentials were invalid Attempt to delete readonly file, Attempt to correct data in bitarchive without permission NetarkivetException UnknownId Identifier could not be resolved. unknown filename or bitarchive name NetarkivetException

Notice that all these exceptions are unchecked. For a detailed dicussion of this see J2EE design and Development, Rod Johnson and Thinking in Java,Bruce Eckel. The arguments against checked exceptions (as recommended by Sun) are:

•Too much code ( Rod Johnson)
•Unreadable code ( Rod Johnson)
•Endless wrapping of exceptions ( Rod Johnson)
•Fragile method signatures ( Rod Johnson)
•Checked exceptions don't always work well with interfaces ( Rod Johnson)
•Experience with large projects show decreased productivity and no increase in code quality (Bruce Eckel)
•Java checked exceptions are more trouble than they are worth (Martin Fowler)

James Gosling replies about exceptions.

For a more balanced overview of the debate, see IBM DeveloperWorks.

Notice that an exception is unchecked does not mean that you don't need to document its usage in JavaDoc for your methods, and you can still add it to the throws clause.

Use exception chaining When an exception is caught and a new exception is thrown, always use the original exception when the new exception is created to ensure that the complete stack-trace is maintained. An example is provided in the next section.

Throw ArgumentNotValid Exception, if public method arguments are invalid For every public method (except some methods that take a HttpServletRequest as argument), we throw ArgumentNotValid exceptions, if arguments are invalid. For that we use the static methods defined in the class ArgumentNotValid:

static public void checkNotNullOrEmpty(String val, String name) { .. }

static public void checkNotNull(Object val, String name) {..}

static public void checkNotNegative(int i, String name) {..}

static public void checkNotNegative(long num, String name) {..}

static public void checkPositive(int i, String name) {..}

static public void checkPositive(long num, String name) {..}

static public void checkNotNullOrEmpty(Collection c, String name) {..}

static public void checkTrue(boolean b, String s) {..}

Hiding implementation specific exceptions A method should not throw checked Exceptions that are implementation specific. If for instance a system to persists configuration data reads and write to the file system, a number of File system exceptions may be thrown. If this implementation is changed to use a database, JDBC exceptions may bee thrown instead. This makes the method signature depend on the actual implementation and should be avoided, instead the exceptions should be caugth and rethrown using one of the standard exceptions in the list above.

class Configuration {

} As an exception, implementations of specific interfaces or abstract classes where throwing a particular exception is part of the contract should still throw that exception, not break the contract.

Meaningful exception texts The text message generated when an exception is thrown should be meaningful to a developer and not include superfluous information. However, any relevant information available when the exception is created should be included, as it eases the debugging.

... // Do not include the name of the exception in the message, // this information is already part of the exception report throw new IOFailure("IOFailure occured"); // WRONG! superfluous message text, with no additional info

// Better text throw new IOFailure("Could not connect to the bitarchive '" + name + "'"); 

To make it easier to notice unintentional whitespace, all string expressions that get embedded in the exception message should be surrounded by single quotes.

Exceptions and JMS JMS cannot carry exceptions over messages. Instead, we have isOk and errorMsg fields on the messages that carry the equivalent information. These fields must be checked and used to throw the appropriate exception, not just dropped.

 . msg = conn.getMsg();
 if (msg != null && msg.isOk()) {
  . return "Got message " + msg;
 } else if (msg != null) {
  . throw new IOFailure("Got error message " + msg.errorMsg());
 } else {
  . throw new IOFailure("Got null message.");
 }

JavaExceptions (last edited 2010-08-16 10:24:28 by localhost)