Exceptional practices, Part 1

Use exceptions effectively in your programs

The proper use of exceptions can make your programs easier to develop and maintain, freer from bugs, and simpler to use. When exceptions are misused, the opposite situation prevails: programs perform poorly, confuse users, and are harder to maintain. To protect against these problems, this series will provide valuable exception-handling techniques. In Part 1, I look at the challenges of using exceptions effectively, and offer guidelines on properly incorporating error handling into your classes at design time. You should think about exceptions and error recovery during the design phase, not after development is complete.

Read the whole series on exception handling:

Using exceptions correctly entails looking at error conditions from several perspectives -- from the perspectives of the class that throws the error, the classes that catch the exception, and the poor user who has to deal with the results. To write friendly programs, you must consider error conditions from all these points of view.

Consider this simple exception class and a method that throws it:

package com.me.mypackage;
public class ResourceLoadException extends Exception {
  public ResourceLoadException(String message) {
    super(message);
  }
}
...
public class ResourceLoader {
  public getResource(String name) throws ResourceLoadException {
  ...
}

When the getResource() method throws a ResourceNotFoundException, it communicates three important pieces of information:

  • The kind of exception that occurred (in this case, a ResourceNotFoundException)
  • The location where the exception occurred, in the form of a stack trace contained within the exception
  • Additional information about the exception, in the form of the message string

Each piece of information will prove important to different recipients. The exception type is important primarily to the classes that called getResource() so the caller can catch certain exceptions and ignore others. The stack trace is significant only to the developer or support technician who may have to isolate or debug the problem. Finally, the message string is most meaningful to the user who must interpret the error message or error log.

When you throw an exception, you should ensure that all recipients receive the information they need to proceed effectively. That means you should throw an exception of the right exception class so that callers can take the appropriate corrective action, and you should generate useful diagnostic messages for users so that they can understand what happened if the program doesn't handle the exception.

Write sensible throws clauses

The set of exception types thrown by a method is an important element of a class's design. When writing a throws clause, you should consider the exception types thrown by a method from the perspective of the method's callers rather than the class's own perspective. What types of exceptions will callers be able to handle sensibly, and what types will they likely just pass on to their caller or to the user?

It's unwise to simply declare a method as throwing all the exceptions that might be thrown by methods it calls. Not only is this an abdication of design responsibility, but it exposes a method's implementation as part of its interface, limiting your ability to modify the implementation in the future. For example, suppose the getResource method can load a resource from a database, from a file, or from a remote server -- exactly where depends on the resource name and how the ResourceLoader object was initialized. If you just let exceptions thrown by called methods propagate out to your callers, getResource() could throw IOException, SQLException, and RemoteException, among others. But will all these different exception types prove useful to the code that calls getResource()? If a method calls getResource() because it needs to load a resource, that method is unlikely to be able to take corrective actions that distinguish between an SQLException and an IOException. From the perspective of the calling method, both exceptions simply mean that the resource couldn't load. Therefore, it probably makes more sense to just throw a single ResourceLoadException exception type.

There is no hard-and-fast rule about how many exception types a method should throw, but fewer is generally better. Each exception type a method might throw will have to either be caught or thrown by any method that calls it, so the more different types of exceptions a method throws, the more difficult that method will be to use. If you throw too many exception types, callers might get lazy and simply catch Exception -- or, worse, throw Exception. These are dangerous practices; callers should instead treat exception types individually. Throwing more than three different exception types generally indicates a problem: the method either performs too many different tasks and should be broken up, or lazily propagates lower-level exceptions that should either be mapped to a single higher-level exception type or caught and handled within the method.

When writing a throws clause, for each distinct exception class that you consider throwing, you should ask yourself: "What would a caller do with this exception? Could the caller possibly take corrective action based on the exception type that differs from any other exception-handling action?" If the answer is no, then that exception should either be caught and handled by your code or translated to another exception type that more closely captures the error type.

Make your throws clauses stable

There is another significant advantage to throwing a small number of higher-level exception types instead of many individual low-level exception types: it prevents the throws clause from changing every time the method changes.

When method signatures change, they often trigger modifications in classes that call them. The impact of those modifications can range from mildly annoying to disastrously inconvenient, depending on how many classes must change and on how many different people or organizations use the altered class. The throws clause provides a vital part of the method signature, and you should take care to ensure its stability. Adding a new exception class to a method's throws clause means that every class using that method must now also catch the new exception or modify its own signature to indicate that the new exception might also be thrown. Clearly, this sort of instability is undesirable and costly. The best way to avoid this problem is to prevent it -- by initially specifying that methods throw exceptions consistent with what they are actually supposed to do, not just with how they are currently implemented. Instead of adding a new exception class to a method's throws clause, try to group related exception types into an object hierarchy and include only the parent exception type in the throws clause. The IOException class from the java.io package provides a good example. More specific exception classes, such as EOFException, are defined as subclasses of IOException, but nearly all the methods in the java.io package are defined as throwing only IOException.

Subclassing exceptions from a common parent allows exception handling to proceed in a more object-oriented manner. Callers can choose to treat all IO-related exceptions equally with a single catch block by simply catching IOException. But if callers can deal with a specific type of IO-related exception in a specific manner, they can catch the more specific exception type first and recover appropriately.

Grouping exception types particular to a package or application together by subclassing them from a common parent improves the stability of methods' throws clauses. You can add specific exception types to the package in future versions without affecting existing methods' signatures, which in turn means that you don't need to change client code every time you add a new exception subclass.

What to do with exceptions you don't want to throw

When your code encounters a low-level exception whose type information would not be meaningful to your callers, translate the exception into one that makes more sense. For example:

public class ResourceLoader {
  public getResource(String name) throws ResourceLoadException {
  try { 
    // try to load the resource from the database 
    ...
  }
  catch (SQLException e) {
    throw new ResourceLoadException(e.toString()):
  }
}

Now, both the program and the user receive the information they need. The caller learns that the resource could not load; this likely provides a more useful type of error than the underlying SQLException, since ResourceLoadException more closely matches the type of operation the caller performed. But the explanatory message from the low-level exception is preserved, so the program can still provide the user with a more specific explanation as to what went wrong.

You can extend the above technique by providing an alternate constructor for ResourceLoadException, one that accepts an Exception as an argument in addition to an error message. Wrapping one exception within another offers a powerful technique (sometimes called exception chaining) for managing complexity while preserving state information. In Part 2 of this series, I'll present a specific technique for exception wrapping that provides the right information to all three interested parties -- the calling method, the developer who must debug the problem, and the user who will encounter it.

Don't forget the user

Though the program doesn't care what text you place in the exception message (and programs should never look at the exception message text and use it to determine what happened), the user does. Error messages should be meaningful both to developers (so they can know more about the error's source and cause) and to users (so they have an inkling of why the program failed). Error messages like "Bad index value" or "Error in initialization" offer no help.

In order to ensure that users understand error messages, you also should think about who those users might be. Will they all speak the same language as the developer? If not, you should use an appropriate method for loading error message strings or templates from some localization mechanism, such as resource bundles, so that error messages can be easily translated into other languages without any text editing.

You should avoid hardcoding English text strings into your code if your classes will eventually be localized for other languages. In fact, even in the absence of a localization requirement, for a number of reasons it's a good idea to get error message strings out of an error catalog or resource bundle and then construct them on the fly. For instance, an exhaustive catalog of all error messages a program might throw will likely make it easier to document your program. The documentation writer can use the catalog as a starting point for writing comprehensive documentation.

Use exceptions properly

Proper exception handling is critically important; but, unfortunately, the ways in which an application or class library generates and handles exceptions is one of the most ignored aspects of program design. Good exception handling practices don't just make your programs more robust and easier to maintain; when something bad happens, the thrown exception might provide the only clue as to what went wrong. If your programs use exceptions sensibly and generate meaningful error messages, the user is less likely to grow more confused and annoyed (at a time when your program has failed, and the user is probably already annoyed). Good exception handling will also make your support staff better able to understand a problem and fix it. Properly using exceptions ensures that all parties -- code and humans alike -- have the error-recovery information they need.

Brian Goetz is a software consultant who has been developing software professionally for more than 15 years. He is a principal consultant at Quiotix, a software development and consulting firm located in Los Altos, Calif.

Learn more about this topic

Join the discussion
Be the first to comment on this article. Our Commenting Policies