Java Tip 134: When catching exceptions, don't cast your net too wide

Understand how Java compilers check catch clauses at compile time

Java's compile-time checking does a pretty good job of keeping exceptions safely caged—you can't call a method that throws a checked exception without catching the exception or declaring that your own method throws that exception. (For a great discussion on checked and unchecked exceptions and when to use each, see "Designing with Exceptions" (JavaWorld, 1998).) The compiler will also sometimes stop you from catching an exception that isn't thrown in the try block, but not always, and not when you need it most. This Java Tip discusses this second compile-time check.

Compile-time checking of throws clauses

First, let's distinguish how Java checks the exceptions a method declares it throws from how it checks the exceptions that a catch clause catches. (In this article, when I say exception with a lowercase e, I mean java.lang.Throwable and its subclasses. When I mean a specific class, like java.lang.Exception, I include the package or at least capitalize the class name.) Initially, the approaches seem quite similar: both indicate the exceptions expected to be thrown by the code block with which they're associated. But while Java requires a method to declare the exceptions that it throws, it doesn't require a method to throw every exception it declares for a good reason: Java allows you to design APIs that remain stable as you add functionality.

Consider this initial version of a home-brewed connection pool:

public class ConnectionPool {
   public ConnectionPool() throws ConnectionException {
   }
   public Connection getConnection() throws ConnectionException {
      // Allocate a connection (possibly throwing a ConnectionException or a
      // subclass) if necessary, then return it
   }
}

While the code in getConnection() might throw a ConnectionException, the constructor does nothing, so in this implementation, the method doesn't really need to declare any exceptions. But in the next version, we might rewrite the class to speed up getConnection():

 public class ConnectionPool {
   public ConnectionPool() throws ConnectionException {
      // Allocate all the connections we think we'll ever need
   }
   public Connection getConnection() throws ConnectionException {
      // Allocate a connection if necessary (not likely), then return it
   }
}

Because we made the constructor in the first version declare ConnectionException, code that uses it doesn't have to change to use the second version. Java trades some checking it could do in the throws clause for the sake of long-term stability in all the other classes that call the constructor—not a bad bargain at all.

Compile-time checking of catch clauses

Catch clauses are a different story from throws clauses. The API stability argument doesn't apply: while a method declaration is part of a class's public interface, a try/catch block is an implementation detail hidden from callers. Not only is there no reason for a catch clause to catch an exception that its try block doesn't throw, guaranteeing that it doesn't do so can catch serious coding errors. For these reasons, Java requires your try blocks to actually throw the exceptions that their catch clauses catch. For example, if you were homesick for your old operating system and wrote the following little utility,

 public class rm {
   public static void main(String[] args) {
      for (int i = 0; i < args.length; i++) {
         try {
            new File(args[i]).delete();
         } catch (IOException e) {     // Won't compile!!!
            System.err.println("rm: Couldn't delete " + args[i]);
         }
      }
   }
}

Sun Microsystems' Java compiler would tell you that IOException "is never thrown in body of corresponding try statement." That is because File.delete() doesn't throw any exceptions at all. Instead, it returns false if it can't delete the file. If it weren't for this compile-time exception checking of the catch clause, you might have accidentally written a program that fails silently.

"But wait a minute," you seasoned coders are thinking, "people catch java.lang.Exception in sloppy code all the time, and the code in those try blocks doesn't always throw java.lang.Exception, but it still compiles!" You're right, and here we come to the goal of our expedition: what rule does Java really use when checking the exceptions that a catch clause is allowed to catch?

Catch clauses catch subclasses too

The answer has two parts: The first is that a catch clause catches exceptions of the type of its parameter and its subclasses. This is a valuable language feature as well: a method that declares it throws, for example, javax.naming.NamingException, can really throw any of NamingException's many subclasses. That method's callers needing to know about a specific subclass can write a catch clause for it; those that don't can simply catch NamingException. Furthermore, if the method's implementation changes in a later version, it can throw NamingException subclasses, which the original implementation did not, and its callers need not change. This flexibility contributes greatly to API stability as well. (See "Exceptional Practices, Part 1" (JavaWorld, 2001) for more discussion on how to design an exception hierarchy and throws clauses.)

The fact that catch clauses catch subclasses can also get you into a bit of trouble. For example, many readers might have written something like this utility method:

 public class ConnectionUtil {
   /** Close the connection silently. Keep going even if there's a problem. */
   public static void close(Connection connection) {
      try {
         connection.close();
      } catch (Exception e) {
         // Log the message (using the JDK 1.4 logger) and move on
         Logger.global.log(Level.WARNING, "Couldn't close connection", e);
      }
   }
}

Errors in closing a connection probably don't mean much to the application, so we just catch and log them. But remember that RuntimeException extends Exception. If we catch Exception, as in the example, we'll catch RuntimeException too. A null Connection passed into this method is possibly a much more serious situation—probably indicating a programming error—than failure to close an otherwise valid Connection. If the attempt to call close() throws NullPointerException, you want the error to propagate up the stack to your serious-error handler, not be mistaken for a warning! The solution? Rather than catching Exception, catch the specific exception thrown by the method you're calling. If it throws more than one exception, catch each individually, or catch a common superclass (as long as it's not Exception).

Be careful with those common superclasses too: while catching Exception is the most common case where compile-time checking will let you down, catching any exception with subclasses can put you into a similar situation. You probably want to handle java.io.FileNotFoundException and java.io.EOFException differently, so don't just catch java.io.IOException and lose the distinction. In general, catch an exception superclass only if it's okay for your code to handle each of its subclasses the same way.

Errors and RuntimeExceptions can be caught whether or not they're thrown

By itself, the fact that catch clauses catch subclasses doesn't explain why the following code fragment compiles, even though the only line of code that might throw an exception has been commented out:

try {
   //doSomething(); // Commented out during development
} catch (Exception e) {
   Logger.global.log(Level.SEVERE, "Something failed", e);
}

The second piece of the puzzle is that two exceptions are unchecked, completely exempt from compile-time checking: java.lang.Error and java.lang.RuntimeException. Catch clauses may catch these exceptions whether or not they're actually thrown. Section 11.2 of the Java Language Specification (JLS) explains why these exceptions may be thrown without being caught or declared: briefly, Errors come from the JVM and may occur anywhere, and RuntimeExceptions come from many language constructs (listed in section 15.6 of the JLS) and may occur almost anywhere. Checking for them would not only prove difficult for compiler writers, but would also force programmers to handle situations they can do nothing about.

Now, back to checking catch clauses: the JLS doesn't say specifically that Error and RuntimeException may be caught whether or not they're actually thrown, but that's how Sun's compiler behaves—an empty try block or one containing only statements that throw no exceptions of any kind may have catch clauses that catch Error or RuntimeException.

Put the pieces together

We still haven't explained why Exception, which is checked, can be caught even when it's apparently not thrown. Put the pieces together: RuntimeException extends Exception, so if RuntimeException can be thrown anywhere, Exception can be thrown anywhere too. Similarly, Error extends Throwable, so although Throwable is checked, it can be caught whether or not it is explicitly thrown.

This logical result of Java's exception-checking rules has an awkward consequence. Everyone first reaches for java.lang.Exception when in a hurry. You know the feeling: you're on deadline, you're struggling with some new toolkit, it has a zillion error conditions, and you just don't have time to deal with them separately. Even if all the toolkit's exceptions extend some common subclass, you're not going to look it up right now. To keep moving, you wrap your code in a try block and catch Exception. Oops: you just laid a trap for yourself or the next person who edits your code. Later, when you refactor your prototype, you'll probably split your big try block. Sometimes, that will result in an exception that was thrown in that try block no longer being thrown. In fact, as in the example above, you might even end up with no code that throws any exceptions at all—but since you're catching Exception, you won't find it out from the compiler. That means useless try/catch blocks hanging around, code bloat, and confusion. So don't catch Exception if you can help it—and if, in a pinch, you do, go back later and tighten up those catch clauses.

Sometimes, catching Exception makes sense, such as in your application's last-ditch error handler or in the execution engine of an application server where you don't want a coding error in one service to bring down the whole server. But most code doesn't need this special treatment. Think twice!

Note that IBM's Java compiler, Jikes, does warn you about catching Throwable or Exception without explicitly throwing them—as well as many other items not caught by Sun's compiler. However, it doesn't warn about catching an exception superclass when only a subclass is thrown. I don't have enough experience with Jikes to recommend using it routinely, but it's easy to try, and I certainly recommend checking it out if you're interested.

Best practices

To sum up, Java allows you to lose error-handling information when you catch an exception: you can catch a superclass instead and lose the specific information conveyed by the subclass. If you catch Exception or Throwable, you won't even be told if your code doesn't throw an exception at all. So, how to stay out of trouble?

  • Catch the most specific exception that you can. Catch a superclass only if you're certain that all the exceptions you'll catch by doing so have the same meaning to your code—and consider what new subclasses future versions of a method might want to throw too. Never catch Exception or Throwable if you can help it.
  • If you're forced to catch Exception or Throwable, consider handling RuntimeException or Error separately from other Exception or Throwable subclasses. And follow the golden rule: never throw Exception or Throwable yourself.
  • As you refactor evolving code, watch for situations where removing code removes possibilities for exceptions to be thrown—the compiler won't always tell you.

With these guidelines in mind, you'll be better prepared to write even more exceptional code.

Dave Schweisguth has written software in the biotechnology industry since 1996 and used Java since 1997. Presently he's a software architect for Applied Biosystems in Foster City, Calif., where he contributes to and evangelizes the company's next-generation service-oriented software architecture.

Learn more about this topic

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