Java supports checked exceptions. This controversial language feature is loved by some and hated by others, to the point where most programming languages avoid checked exceptions and support only their unchecked counterparts.
In this post, I examine the controversy surrounding checked exceptions. I first introduce the exceptions concept and briefly describe Java's language support for exceptions to help beginners better understand the controversy.
What are exceptions?
In an ideal world, computer programs would never encounter any problems: files would exist when they are supposed to exist, network connections would never close unexpectedly, there would never be an attempt to invoke a method via the null reference, integer-division-by-zero attempts wouldn't occur, and so on. However, our world is far from ideal; these and other exceptions to ideal program execution are widespread.
Early attempts to recognize exceptions included returning special values that indicate failure. For example, the C language's fopen()
function returns NULL
when it cannot open a file. Also, PHP's mysql_query()
function returns FALSE
when an SQL failure occurs. You have to look elsewhere for the actual failure code. Although easy to implement, there are two problems with this "return special value" approach to recognizing exceptions:
- Special values don't describe the exception. What does
NULL
orFALSE
really mean? It all depends on the author of the functionality that returns the special value. Furthermore, how do you relate a special value to the program's context when the exception occurred so that you can present a meaningful message to the user? - It's too easy to ignore a special value. For example,
int c; FILE *fp = fopen("data.txt", "r"); c = fgetc(fp);
is problematic because this C code fragment executesfgetc()
to read a character from the file even whenfopen()
returnsNULL
. In this case,fgetc()
won't succeed: we have a bug that may be hard to find.
The first problem is solved by using classes to describe exceptions. A class's name identifies the kind of exception and its fields aggregate appropriate program context for determining (via method calls) what went wrong. The second problem is solved by having the compiler force the programmer to either respond to an exception directly or indicate that the exception is to be handled elsewhere.
Some exceptions are very serious. For example, a program might attempt to allocate some memory when no free memory is available. Boundless recursion that exhausts the stack is another example. Such exceptions are known as errors.
Exceptions and Java
Java uses classes to describe exceptions and errors. These classes are organized into a hierarchy that's rooted in the java.lang.Throwable
class. (The reason why Throwable
was chosen to name this special class will become apparent shortly.) Directly underneath Throwable
are the java.lang.Exception
and java.lang.Error
classes, which describe exceptions and errors, respectively.
For example, the Java library includes java.net.URISyntaxException
, which extends Exception
and indicates that a string couldn't be parsed as a Uniform Resource Identifier reference. Note that URISyntaxException
follows a naming convention in which an exception class name ends with the word Exception
. A similar convention applies to error class names, such as java.lang.OutOfMemoryError
.
Exception
is subclassed by java.lang.RuntimeException
, which is the superclass of those exceptions that can be thrown during the normal operation of the Java Virtual Machine (JVM). For example, java.lang.ArithmeticException
describes arithmetic failures such as attempts to divide integers by integer 0. Also, java.lang.NullPointerException
describes attempts to access object members via the null reference.
When an exception or error occurs, an object from the appropriate Exception
or Error
subclass is created and passed to the JVM. The act of passing the object is known as throwing the exception. Java provides the throw
statement for this purpose. For example, throw new IOException("unable to read file");
creates a new java.io.IOException
object that's initialized to the specified text. This object is subsequently thrown to the JVM.
Java provides the try
statement for delimiting code from which an exception may be thrown. This statement consists of keyword try
followed by a brace-delimited block. The following code fragment demonstrates try
and throw
:
try
{
method();
}
// ...
void method()
{
throw new NullPointerException("some text");
}
In this code fragment, execution enters the try
block and invokes method()
, which throws an instance of NullPointerException
.
The JVM receives the throwable and searches up the method-call stack for a handler to handle the exception. Exceptions not derived from RuntimeException
are often handled; runtime exceptions and errors are rarely handled.
A handler is described by a catch
block that follows the try
block. The catch
block provides a header that lists the types of exceptions that it's prepared to handle. If the throwable's type is included in the list, the throwable is passed to the catch
block whose code executes. The code responds to the cause of failure in such a way as to cause the program to proceed, or possibly terminate:
try
{
method();
}
catch (NullPointerException npe)
{
System.out.println("attempt to access object member via null reference");
}
// ...
void method()
{
throw new NullPointerException("some text");
}
In this code fragment, I've appended a catch
block to the try
block. When the NullPointerException
object is thrown from method()
, the JVM locates and passes execution to the catch
block, which outputs a message.
Exceptions described by Exception
and its subclasses except for RuntimeException
and its subclasses are known as checked exceptions. For each throw
statement, the compiler examines the exception object's type. If the type indicates checked, the compiler checks the source code to ensure that the exception is handled in the method where it's thrown or is declared to be handled further up the method-call stack. All other exceptions are known as unchecked exceptions.
Java lets you declare that a checked exception is handled further up the method-call stack by appending a throws
clause (keyword throws
followed by a comma-delimited list of checked exception class names) to a method header:
try
{
method();
}
catch (IOException ioe)
{
System.out.println("I/O failure");
}
// ...
void method() throws IOException
{
throw new IOException("some text");
}
Because IOException
is a checked exception type, thrown instances of this exception must be handled in the method where they are thrown or be declared to be handled further up the method-call stack by appending a throws
clause to each affected method's header. In this case, a throws IOException
clause is appended to method()
's header. The thrown IOException
object is passed to the JVM, which locates and transfers execution to the catch
handler.
Arguing for and against checked exceptions
Checked exceptions have proven to be very controversial. Are they a good language feature or are they bad? In this section, I present the cases for and against checked exceptions.
Checked exceptions are good
James Gosling created the Java language. He included checked exceptions to encourage the creation of more robust software. In a 2003 conversation with Bill Venners, Gosling pointed out how easy it is to generate buggy code in the C language by ignoring the special values that are returned from C's file-oriented functions. For example, a program attempts to read from a file that wasn't successfully opened for reading.
Gosling also pointed out that college programming courses don't adequately discuss error handling (although that may have changed since 2003). When you go through college and you're doing assignments, they just ask you to code up the one true path [of execution where failure isn't a consideration]. I certainly never experienced a college course where error handling was at all discussed. You come out of college and the only stuff you've had to deal with is the one true path.
Focusing only on the one true path, laziness, or another factor has resulted in a lot of buggy code being written. Checked exceptions require the programmer to consider the source code's design and hopefully achieve more robust software.
Checked exceptions are bad
Many programmers hate checked exceptions because they're forced to deal with APIs that overuse them or incorrectly specify checked exceptions instead of unchecked exceptions as part of their contracts. For example, a method that sets a sensor's value is passed an invalid number and throws a checked exception instead of an instance of the unchecked java.lang.IllegalArgumentException
class.
Here are a few other reasons for disliking checked exceptions; I've excerpted them from Slashdot's Interviews: Ask James Gosling About Java and Ocean Exploring Robots discussion:
- Checked exceptions are easy to ignore by rethrowing them as
RuntimeException
instances, so what's the point of having them?I've lost count of the number of times I've written this block of code:
try { // do stuff } catch (AnnoyingcheckedException e) { throw new RuntimeException(e); }
99% of the time I can't do anything about it. Finally blocks do any necessary cleanup (or at least they should).
- Checked exceptions can be ignored by swallowing them, so what's the point of having them?
I've also lost count of the number of times I've seen this:
try { // do stuff } catch (AnnoyingCheckedException e) { // do nothing }
Why? Because someone had to deal with it and was lazy. Was it wrong? Sure. Does it happen? Absolutely. What if this were an unchecked exception instead? The app would've just died (which is preferable to swallowing an exception).
- Checked exceptions result in multiple
throws
clause declarations.The problem with checked exceptions is they encourage people to swallow important details (namely, the exception class). If you choose not to swallow that detail, then you have to keep adding
throws
declarations across your whole app. This means 1) that a new exception type will affect lots of function signatures, and 2) you can miss a specific instance of the exception you actually -want- to catch (say you open a secondary file for a function that writes data to a file. The secondary file is optional, so you can ignore its errors, but because the signature throwsIOException
, it's easy to overlook this). - Checked exceptions are not really exceptions.
The thing about checked exceptions is that they are not really exceptions by the usual understanding of the concept. Instead, they are API alternative return values.
The whole idea of exceptions is that an error thrown somewhere way down the call chain can bubble up and be handled by code somewhere further up, without the intervening code having to worry about it. Checked exceptions, on the other hand, require every level of code between the thrower and the catcher to declare they know about all forms of exception that can go through them. This is really little different in practice to if checked exceptions were simply special return values which the caller had to check for.
Additionally, I've encountered the argument about applications having to handle large numbers of checked exceptions that are generated from the multiple libraries that they access. However, this problem can be overcome through a cleverly-designed facade that leverages Java's chained-exception facility and exception rethrowing to greatly reduce the number of exceptions that must be handled while preserving the original exception that was thrown.
Conclusion
Are checked exceptions good or are they bad? In other words, should programmers be forced to handle checked exceptions or given the opportunity to ignore them? I like the idea of enforcing more robust software. However, I also think that Java's exception-handling mechanism needs to evolve to make it more programmer-friendly. Here are a couple of ways to improve this mechanism: