by Jean-Pierre Norguet

Exceptions for action

how-to
Nov 15, 200711 mins

Exception design for efficient error handling

Exceptions in object-oriented applications tend to proliferate, overload the code, and improperly handle issues. In this article, author Jean-Pierre Norguet explains how to design exceptions in order to implement a simple, readable, robust, active, debug-oriented, and user-friendly error-handling system. He proposes the design of a sample exception set, including the source code of a Java implementation. Finally, he explains how to integrate such a design into a Java enterprise application.

The best way to design exceptions in an object-oriented project is never as clear as we would like it to be. Exceptions tend to proliferate in older and larger systems, eventually amounting to hundreds of lines of code. Checked exceptions are required for some common programming scenarios but can create significant processing overhead. And silent catching has been shown to be a source of trouble. Unfortunately, you cannot avoid these mishaps; you must, instead, learn to code your way through them.

In this article I show you that it is possible to satisfy the requirements of an error-handling system with a limited set of exceptions. After establishing the foundation of a good error-handling system, I point out some of the common mistakes in exception design that can undermine application performance. I then present a sample exception set that supports my basic premise: that exceptions intended to help an external system (or user) deal with unexpected conditions should be designed differently from those intended to help the programmer handle expected ones. I explain the semantics of my exception set and also show how the various exceptions would flow through a typical Java enterprise application architecture. Finally, I show you how to implement my exception set in Java.

Requirements of error handling

What is a good error-handling system? Besides aesthetic considerations, a good error-handling system is generally held to meet the following requirements:

  • It is triggered without failure in case of any kind of error in the system.
  • It allows the application to automatically take the appropriate actions.
  • The error message displayed to the user gives a clear description of what is wrong and what action must be taken to proceed.
  • If assistance is required, the error message helps the user to interact with the help desk and gives the help-desk team the necessary information to react easily and quickly.
  • The logged information gives the development team the necessary information to identify the error, locate its source point in the application code, and fix the issue.
  • The error-handling code should not degrade code readability. While essential, error handling is only a safety net, and therefore of less priority than the application’s primary functions.

An error-handling system designed to meet these requirements is generally considered to be complete. The question that remains for many Java developers is how to design an error-handling system that makes intelligent use of exceptions, and that is not overloaded by them.

Common practices to avoid

It is possible to satisfy the above requirements with a limited set of exceptions. When designing such an exception set, you will want to avoid a number of common practices. For instance:

  • Defining an exception class for each issue that may arise in the system tends to exception proliferation.
  • Defining a complete exception set in each package is not always useful and also encourages exception proliferation.
  • Providing both a checked and a non-checked version of an exception introduces exception overhead. The subsequent duplication of exception semantics also makes for a confusing exception design.
  • Finally, throwing and catching generic exceptions is error-prone in many aspects.

The exception set I propose in the next section dispatches error-handling semantics in such a way as to avoid these common problems associated with exception handling, especially when applications grow in size and complexity.

Design for action

Figure 1 shows an exception set designed to avoid exception multiplication, checked-exception overhead, and silent catching.

A sample exception set.
Figure 1. A sample exception set (click for a larger image)

You will note that some of the exceptions in Figure 1 are checked, while others are runtime. To avoid the throw-declaration overhead, checked exceptions are limited to two purposes:

  1. Telling a calling method that an exceptional but foreseen condition occurred during processing. In this case, the semantics of the issue is defined by the processing method; immediate catching is appropriate.
  2. Telling an external system that an unresolved issue has occurred and that it should handle the issue according to the exception semantics.

Understanding exception semantics

I should clarify here my definition of exception semantics. In my view, classes of objects should be designed according to their real-world equivalents. A fruit or a person can be designed easily as a Java object class. Exception design is different: while it is clear what a fruit or a person is in the real world, the same cannot be said of an exception. Actually, from the two exception categories above, only the first one exists in the real world. Exceptions in the second category model what can go wrong during system execution, and therefore do not exist outside the system. Immediate catching is thus appropriate only for exceptions in the first category.

My design proposal is based on the idea that exceptions should be designed according to their purpose. Exceptions that are internal to the system and self-regulatory are meant to help the system react in the case of unexpected conditions. The purpose of these exceptions is therefore not to model the issue at hand, but to give an indication of what kind of action the system must take.

Sample exception set

In Figure 1 you see four kinds of exceptions designed to take four kinds of action, as follows:

  1. BusinessException: An exceptional condition has occurred. This condition was foreseen and can be checked by the calling method for immediate action.
  2. ParameterException: The data entered does not allow for proper processing. The user must be asked to re-enter valid data or to modify the conditions in which processing occurs.
  3. TechnicalException: A technical issue like an invalid SQL statement has occurred. The requested operation cannot be fulfilled. The user should contact the help desk for investigation or try another service. The use of the application by other users is not impacted.
  4. CriticalTechnicalException: A technical issue like a database crash has occurred. In these conditions, the whole application is unusable. The user should be encouraged to retry later. Other users should not use the application until it has been repaired.

This set of exceptions is only one example; many other exception sets could be defined similarly. For example, TechnicalException and CriticalTechnicalException could be designed as a single exception class with a boolean severity attribute. What is important is to focus on the kind of action that should be taken, rather than on the issue that raised the exception.

Exception logging

Although the exception semantics focus on the action to take, the issue that has been raised is also important. The development team could, for example, use this information to debug the code. In my exception design, information about the cause of the exception can be found in the application’s error log file. With a good logging framework in place, it should be sufficient to log information about the issue from the exception message and stack trace.

The only issue that remains in how to design the exception so that this information can be easily retrieved. One solution is to provide the exception with an id attribute representing the kind of issue at hand. Also, if the issue has thrown its own exception, this exception can be nested into the application exception. At catching time, the original message and stack trace can be retrieved from the nested exception. The id attribute and exception nesting are two ways to include issue-related information in the exception itself.

Designing the flow of exceptions

Once you have designed the exceptions themselves, the next step is to think about how they will flow through your application. A standard JEE application architecture is mainly composed of four packages: presentation, business, integration, and persistence. Exceptions are typically thrown by the integration and persistence packages. In the business package, the inner runtime layers catch checked exceptions as soon as they can, while the outer layers catch the runtime exceptions and take appropriate actions according to their class. You can also throw and catch some checked exceptions inside the business package. In this scheme, the responsibility of the integration and persistence packages, as well as of the business package’s inner layer, is to convert runtime exceptions into actions. A typical JEE application architecture of this sort is shown in Figure 2.

Figure 2. Exception flow in a standard JEE package architecture (click for a larger image)

The path of an exception thrown from the persistence package (for example) depends on where the issue can be resolved. If the calling method can resolve the issue, the exception is caught immediately, the appropriate action is taken, and the business flows on normally. If the issue cannot be resolved, the exception is nested into a runtime exception and passed on silently through the business package’s intermediate layers into to the upper layers of the application. In these layers, typically by some kind of application controller, the runtime exception is caught, the appropriate action is taken, and the presentation layer displays the corresponding error message to the user. Immediate catching of checked exceptions and late catching of runtime exceptions are the two main scenarios in this kind of exception design, as shown in Figure 3.

Figure 3. Immediate catching of a checked exception and late catching of a runtime exception (click for a larger image)

Extending from java.lang.Exception

The exception design I propose can be easily implemented in any object-oriented language, including Java. In fact, a similar exception tree is already provided in the standard Java library. In this library, exceptions are modeled as java.lang.Throwable, checked exceptions are modeled as java.lang.Exception, and runtime exceptions are modeled as java.lang.RuntimeException.

Under java.lang.Exception, a number of business exceptions with a broad panel of semantics are provided. Runtime application exceptions like ParameterException, TechnicalException, and CriticalTechnicalException (shown in Figure 1), are respectively and roughly modeled as IllegalArgumentException, MissingResourceException, and IllegalStateException.

In your application, reusing standard Java exceptions is an interesting idea, but it could introduce some confusion with the exceptions thrown by the standard Java classes. You could avoid this confusion by defining your own tree, extending java.lang.Exception. Defining your tree also allows you to implement exception nesting and issue IDs.

Listing 1 shows my sample exception set implemented in Java code. Note that it includes exception nesting and id attributes.

Listing 1. Java code implementing exception nesting and issue identification

public class NestedException extends RuntimeException {
   protected Exception nestedException;
   protected int issueId;
   public NestedException(String msg, Exception e, int id) {
      super(msg);
      this.nestedException = e;
      this.issueId = id;
   }
   public Exception getNestedException() {
      return this.nestedException;
   }
   public int getIssue() {
      return this.issueId;
   }
}
public interface Issue {
   public final static int UNDEFINED = 0;
   public final static int EXTERNAL_SERVICE_1_DOWN = 1;
   public final static int EXTERNAL_SERVICE_2_DOWN = 2;
   public final static int SQL_STATEMENT_ERROR = 3;
   // ...
}

In conclusion

Designing a limited exception tree that fulfills the requirements of a good error-handling system is very simple. The secret of this simplicity resides in focusing your design effort on defining the kinds of actions the system should take, rather than on the kinds of issues that may arise. In this design, information about the issue is encapsulated inside the exception. Dispatching the semantics between actions and issues this way allows you to narrow the exception tree to a limited set of exceptions — perhaps a half dozen. Not only does this design limit exception proliferation but it keeps your code readable, which subsequently allows you to focus on coding the application’s business logic with optimal clarity.

Jean-Pierre Norguet holds a Ph.D. in computer science and network engineering from the University of Brussels. After three years of full-time Java development with IBM on mission-critical e-business applications, as team leader and coach, his areas of expertise grew to include the entire application development lifecycle. His research currently focuses on Java technology, ontology integration, and Web usage mining. In addition to several Java articles online, Dr. Norguet has published JEE books with Prentice Hall and IBM Press, as well as several articles in international research conferences of ACM, IEEE, and Springer-Verlag. His outside interests include artistic drawing, French theater acting, and alternative healthcare.