Java theory and practice: The exceptions debate

To check, or not to check?

Most of the advice on the use of exceptions in the Java language suggests that checked exceptions should be preferred in any case where an exception conceivably might be caught. This suggestion is encouraged by both the language design (in that the compiler forces you to list in the method signature all checked exceptions that might be thrown) and in early writings on style and usage. Recently, several prominent writers have started to come to the position that unchecked exceptions may have more of a place in good Java class design than previously thought. In this article, Brian Goetz looks at the pros and cons of using unchecked exceptions.

Brian Goetz (brian@quiotix.com), Principal Consultant, Quiotix Corp

Brian Goetz has been a professional software developer for over 15 years. He is a Principal Consultant at Quiotix, a software development and consulting firm located in Los Altos, California, and he serves on several JCP Expert Groups. See Brian's published and upcoming articles in popular industry publications.



25 May 2004

Also available in Russian Japanese

Like C++, the Java language provides for throwing and catching of exceptions. Unlike C++, though, the Java language supports both checked and unchecked exceptions. Java classes must declare any checked exceptions they throw in the method signature, and any method that calls a method that throws a checked exception of type E must either catch E or must also be declared to throw E (or a superclass of E). In this way, the language forces us to document all the anticipated ways in which control might exit a method.

To free developers of dealing with the sorts of exceptions that occur as a result of programming errors or that the program could not be expected to catch (dereferencing a null pointer, falling off the end of an array, dividing by zero, and so on), some exceptions are nominated as unchecked exceptions (those that derive from RuntimeException) and do not need to be declared.

Conventional wisdom

The conventional wisdom for whether to declare an exception as checked or unchecked is summarized in the following excerpt of "The Java Tutorial" from Sun (see Resources for more information):

Because the Java language does not require methods to catch or specify runtime exceptions, it's tempting for programmers to write code that throws only runtime exceptions or to make all of their exception subclasses inherit from RuntimeException. Both of these programming shortcuts allow programmers to write Java code without bothering with all of the nagging errors from the compiler and without bothering to specify or catch any exceptions. While this may seem convenient to the programmer, it sidesteps the intent of Java's catch or specify requirement and can cause problems for the programmers using your classes.

Checked exceptions represent useful information about the operation of a legally specified request that the caller may have had no control over and that the caller needs to be informed about -- for example, the file system is now full, or the remote end has closed the connection, or the access privileges don't allow this action.

What does it buy you if you throw a RuntimeException or create a subclass of RuntimeException just because you don't want to deal with specifying it? Simply, you get the ability to throw an exception without specifying that you do so. In other words, it is a way to avoid documenting the exceptions that a method can throw. When is this good? Well, when is it ever good to avoid documenting a method's behavior? The answer is "hardly ever."

In other words, Sun is telling us that checked exceptions should be the norm. The tutorial goes on to say, in several different ways, that you should generally throw Exception and not throw RuntimeException -- unless you're the JVM.

In his book Effective Java: Programming Language Guide (see Resources), Josh Bloch offers the following bits of wisdom regarding checked and unchecked exceptions, which are consistent with (but not quite as strict as) what "The Java Tutorial" suggests:

  • Item 39: Use exceptions only for exceptional conditions. That is, do not use exceptions for control flow, such as catching NoSuchElementException when calling Iterator.next() instead of first checking Iterator.hasNext().
  • Item 40: Use checked exceptions for recoverable conditions and runtime exceptions for programming errors. Here, Bloch echoes the conventional Sun wisdom -- that runtime exceptions should be used only to indicate programming errors, such as precondition violations.
  • Item 41: Avoid unnecessary use of checked exceptions. In other words, don't use checked exceptions for conditions from which the caller could not possibly recover, or for which the only foreseeable response would be for the program to exit.
  • Item 43: Throw exceptions appropriate to the abstraction. In other words, exceptions thrown by a method should be defined at an abstraction level consistent with what the method does, not necessarily with the low-level details of how it is implemented. For example, a method that loads resources from files, databases, or JNDI should throw some sort of ResourceNotFound exception when it cannot find a resource (generally using exception chaining to preserve the underlying cause), rather than the lower-level IOException, SQLException, or NamingException.

Reexamining the checked exception orthodoxy

Recently, several well-regarded experts, including Bruce Eckel and Rod Johnson, have publicly stated that while they initially agreed completely with the orthodox position on checked exceptions, they've concluded that exclusive use of checked exceptions is not as good an idea as it appeared at first, and that checked exceptions have become a significant source of problems for many large projects. Eckel takes a more extreme view, suggesting that all exceptions should be unchecked; Johnson's view is more conservative, but still suggests that the orthodox preference for checked exceptions is excessive. (It's worth noting that the architects of C#, who almost certainly had plenty of experience using Java technology, chose to omit checked exceptions from the language design, making all exceptions unchecked exceptions. They did, however, leave room for an implementation of checked exceptions at a later time.)


Some criticisms of checked exceptions

Both Eckel and Johnson point to a similar list of problems with checked exceptions; some are inherent properties of checked exceptions, some are properties of the particular implementation of checked exceptions in the Java language, and some are simply observations about how the widespread misuse of checked exceptions has become such a significant problem that perhaps the mechanism needs to be rethought.

Checked exceptions inappropriately expose the implementation details

How many times have you seen (or written) a method that throws SQLException or IOException, even though it appears to have nothing to do with databases or files? It is quite common for a developer to simply tally up all the exceptions that might be thrown in a method's initial implementation and add them to the throws clause of the method (many IDEs will even help you perform this task). One problem with the pass-through approach is that it violates Bloch's Item 43 -- the exceptions being thrown are at an abstraction level that is inconsistent with the method throwing them.

A method whose job it is to load a user profile should throw NoSuchUserException when it can't find the user, not SQLException -- the caller might well expect that the user might not be found, but has no idea what to do with SQLException. Exception chaining can be used to throw a more appropriate exception without throwing away the details (such as the stack trace) of the underlying failure, allowing abstract layers to insulate the layers above them from the details of the layers below them while preserving information that might be useful for debugging.

That said, packages like JDBC are designed in such a way as to make it difficult to avoid this problem. Every method in the JDBC interface throws SQLException, but several different types of problems could be experienced in the course of accessing a database, and different methods may be susceptible to different error modes. A SQLException might indicate a system-level problem (cannot make a connection to the database), a logical problem (no more rows in the result set), or a problem with the specific data (the primary key for the row you just tried to insert already exists or violates an entity integrity constraint). Callers cannot differentiate between these different types of SQLExceptions without committing the unpardonable sin of trying to parse the message text. (SQLException does expose methods for fetching the database-specific error code and SQL state variables, but in practice these are rarely used to differentiate among different database error conditions.)

Unstable method signatures

The problem of unstable method signatures is related to the previous problem -- if you are simply passing exceptions through a method, then every time you change the implementation of a method you have to change its method signature, as well as change all the code that calls it. Managing fragile method signatures becomes an expensive proposition once a class is already deployed in production. However, this problem is basically another symptom of the failure to follow Bloch's Item 43. A method should throw an exception when it experiences a failure, but that exception should reflect what the method does, not how it does it.

Sometimes, when programmers get tired of adding and removing exceptions from method signatures as a result of implementation changes, instead of using an abstraction to define the types of exceptions a given layer might throw, they simply declare all their methods to throw Exception. In other words, they have decided that exceptions are just too much of a pain and basically turned them off. Needless to say, this approach is generally not a good error handling strategy for all but the most disposable code.

Unreadable code

Because many methods throw a number of different exceptions, the ratio of lines of error-handling code to actual do-something code can be high, making it difficult to find the code that actually does something in a method. Exceptions were supposed to make code smaller by centralizing error handling, but a method with three lines of code and six catch blocks (each of which either just logs the exception or wraps and rethrows it) seems kind of bloated and can obfuscate otherwise simple code.

Exception swallowing

We've all seen code where an exception is caught but there is no code inside the catch block. While this programming practice is clearly a bad one, it is easy to see how it comes about -- during prototyping, someone wrapped the code with a try...catch block and forgot to go back later and fill in the catch block. While this error is a common one, it's also one of the places where better tools can save us -- it is easy for editors, compilers, or static inspection tools to detect and issue a warning where an exception is being swallowed.

An overly general try...catch block is another form of exception swallowing that is harder to detect, and is the result of the (questionable) structure of the class hierarchy of exceptions in the Java class library. Let's say a method throws four different types of exceptions, and a caller that encounters any of them is going to catch them, log them, and return. One way to implement this strategy is with a try...catch block with four catch clauses, one for each type of exception. To avoid the unreadable-code problem, some developers will refactor this code, as in Listing 1:

Listing 1. Accidentally swallowing RuntimeException
try { 
  doSomething();
}
catch (Exception e) { 
  log(e);
}

While this code is more compact than the four catch blocks, it has a problem -- it also captures any RuntimeExceptions that might have been thrown by doSomething and prevents them from being propagated.

Too much exception wrapping

If an exception is generated in a low-level facility, and propagates up through many layers of code, it may be caught, wrapped, and rethrown several times before it is ultimately handled. When the exception is eventually logged, the stack trace may be many pages, as the stack trace will be duplicated several times, once for each layer of wrapping. (The implementation of exception chaining in JDKs 1.4 and later mitigates this problem somewhat.)


Alternate approaches

Bruce Eckel, author of Thinking in Java (see Resources), says that after years of using the Java language, he has come to the conclusion that checked exceptions were a mistake -- an experiment that should be declared a failure. Eckel advocates making all exceptions unchecked, and offers the class in Listing 2 as a means for turning checked exceptions into unchecked ones while preserving the ability to catch specific types of exceptions as they are propagated up the stack (see his article in the Resources section for an explanation of how it can be used):

Listing 2. Eckel's exception adapter class
class ExceptionAdapter extends RuntimeException {
  private final String stackTrace;
  public Exception originalException;
  public ExceptionAdapter(Exception e) {
    super(e.toString());
    originalException = e;
    StringWriter sw = new StringWriter();
    e.printStackTrace(new PrintWriter(sw));
    stackTrace = sw.toString();
  }
  public void printStackTrace() { 
    printStackTrace(System.err);
  }
  public void printStackTrace(java.io.PrintStream s) { 
    synchronized(s) {
      s.print(getClass().getName() + ": ");
      s.print(stackTrace);
    }
  }
  public void printStackTrace(java.io.PrintWriter s) { 
    synchronized(s) {
      s.print(getClass().getName() + ": ");
      s.print(stackTrace);
    }
  }
  public void rethrow() { throw originalException; }
}

If you follow the discussion on Eckel's Web site, you'll find the responders deeply divided. Some think his proposal is ridiculous; some think it's a great idea. (My opinion is that, while properly using exceptions certainly has its challenges and that bad examples of exception usage abound, most of the people who agree with him are doing so for the wrong reason, in the same way that a politician who ran on a platform of universal subsidized access to chocolate would get a lot of votes from 10-year-olds.)

Rod Johnson, author of J2EE Design and Development (see Resources), which is one of the best books I've read on Java development, J2EE or not, takes a less radical approach. He enumerates several categories of exceptions, and identifies a strategy for each. Some exceptions are basically secondary return codes (which generally signal violation of business rules), and some are of the "something went horribly wrong" variety (such as failure to make a database connection). Johnson advocates using checked exceptions for the first category (alternative return codes), and runtime exceptions for the latter category. In the "something went horribly wrong" category, the motivation is simply to recognize the fact that no caller is going to effectively handle this exception, so it might as well get propagated all the way up the stack with the minimum of impact on the intervening code (and minimize the chance of exception swallowing).

Johnson also enumerates a middle ground, for which he asks the question, "Will only a minority of callers want to handle the problem?" For these scenarios, he also suggests using unchecked exceptions. As an example for this category, he lists JDO exceptions -- most of the time, JDO exceptions indicate a condition that the caller will not want to handle, but some cases occur where specific types of exceptions might usefully be caught and handled. Rather than make the rest of the JDO-consuming classes pay for this possibility in the form of catching and rethrowing these exceptions, he suggests using unchecked exceptions here instead.

Using unchecked exceptions

The decision to use unchecked exceptions is a complicated one, and it's clear that there's no obvious answer. The Sun advice is to use them for nothing, the C# approach (which Eckel and others agree with) is to use them for everything. Others say, "there's a middle ground."

Having used exceptions in C++, where all exceptions are unchecked, I have found that one of the biggest risks of unchecked exceptions is that they are not self-documenting in the way checked exceptions are. Unless the creator of the API explicitly documents the exceptions thrown, callers have no way to know what exceptions to catch in their code. Unfortunately, my experience is that most C++ APIs are very poorly documented, and even well-documented ones lack sufficient information on what exceptions might be thrown from a given method. I don't see any reason why this problem wouldn't be equally common with Java class libraries that rely heavily on unchecked exceptions. It's hard enough to rely on your own programming skill or that of your co-workers; it's scary to have to rely on the documentation skill of anyone whose code you might be using sixteen frames down the call stack as your primary error-handling mechanism.

The documentation issue further underscores why laziness is a bad reason to choose to use unchecked exceptions, because the documentation burden on packages that use unchecked exceptions should be even higher than with checked exceptions (as it becomes even more important to document the unchecked exceptions you throw than with checked exceptions).


Document, document, document

If you do decide to use unchecked exceptions, you need to document this choice thoroughly, including documenting in Javadoc all the unchecked exceptions a method might throw. Johnson suggests making the decision between checked and unchecked exceptions on a package-by-package basis. When using unchecked exceptions, also remember that you may need to use try...finally blocked even when you are not catching any exceptions, so that you can perform cleanup actions such as closing database connections. With checked exceptions, we have try...catch to remind us to add a finally clause. With unchecked exceptions, we don't have that crutch to lean on.

Resources

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into Java technology on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Java technology
ArticleID=10946
ArticleTitle=Java theory and practice: The exceptions debate
publish-date=05252004