Does Java need Checked Exceptions?
Although C++ introduced the exception specification, Java is the only mainstream
language that enforces the use of specifications with "Checked Exceptions." In
this discussion I will examine the motivation and outcome of this experiment,
and look at alternative (and possibly more useful) ways to manage exceptions.
The goal of this discussion is to explore the ideas -- in particular, what are
your experiences with exceptions? Would it be more beneficial to you to use
un-checked exceptions?
Note: You can find an associated essay
here, and a related article
here.
I began learning about exceptions when they were introduced into the C++
committee, and it's been a long learning curve. One of the first justifications
for exceptions was that they would allow programmers to write less error
checking code because this code could be delayed until a more appropriate point
in the program, rather than having to put tests at the point of every function
call -- which no one was doing anyway. In fact, I think it was the poor
error-handling model that C brought in that was the major motivation for
exception handling in C++, because what we really needed was a unified and
consistent way of reporting errors (unfortunately, because C++ is
backwards-compatible with C, exception handling in C++ is simply an
additional error handling model).
Checked exceptions seem like a really good idea at first. But it's all based
on our unchallenged assumption that static type checking detects your problems
and is always best. Java is the first language (that I know of) that uses
checked exceptions and is thus the first experiment. However, the kind of code
you must write around these things and the common phenomenon of "swallowed"
exceptions begins to suggest there's a problem. In Python, exceptions are
unchecked, and you can catch them and do something if you want, but you aren't
forced. And it seems to work just fine.
I think it's a compile-time vs. run-time checking issue. We have gotten so used
to thinking that the only correct way, only safe and reliable way, to do things
is at compile time, that we automatically discount any solutions that rely on
run-time as unreliable. That thinking came from C++, but I note that Java
actually does a fair number of things at runtime, which we accept merely because
they can't be done at compile time. But despite that we still hold this idea
that if it can be done at compile time, then that's the only proper time to do
it (I am also referring here to weak typing in Python). I know this seems like a
less precise and provable way of thinking, but if you start having experiences
that seem to disprove the common way of thinking then you start questioning it.
I began having discussions about this last Summer, and which I've started
hearing from other people about -- that checked exceptions were a mistake.
They're not in Python or C#, or C++. In fact, the only language I know of where
they exist is in Java, and I'll bet it was because people saw unchecked
exception specifications in C++ and thought that was a mistake (I know I
did, for the longest time). At this point, I feel like checked exceptions are
(1) an untried experiment when they were put into Java (unless you know of some
other language where it is implemented ... Ada, perhaps?) (2) a failure because
so many people end up swallowing the exceptions in their code.
In the Python/C# approach, the exception is thrown, and if you want to you can
write code to catch it, but if you don't you aren't forced to write a bunch of
extra code and be tempted to swallow the exception.
I currently plan to rewrite the Exceptions chapter (and the rest of the book)
for the 3rd edition to change the way exceptions are handled.
The way I (now) see exceptions is something like this:
1) The great value of exceptions is the unification of error reporting: a
standard mechanism by which to report errors, rather than the potpourri of
ignorable approaches that we had in C (and thus, C++, which only adds exceptions
to the mix, and doesn't make it the exclusive approach). The big advantage Java
has over C++ is that exceptions are the only way to report errors.
2) "Ignorable" in the previous paragraph is the other issue. The theory is that
if the compiler forces the programmer to either handle the exception or pass it
on in an exception specification, then the programmer's attention will always be
brought back to the possibility of errors and they will thus properly take care
of them. I think the problem is that this is an untested assumption we're making
as language designers that falls into the field of psychology. My theory is that
when someone is trying to do something and you are constantly prodding them with
annoyances, they will use the quickest device available to make those annoyances
go away so they can get their thing done, perhaps assuming they'll go back and
take out the device later. I discovered I had done this in the first edition of
Thinking in Java:
...
} catch (SomeKindOfException e) {}
And then more or less forgot it until the rewrite. How many people thought this
was a good example and followed it? I began seeing the same kind of code,
and realized people were stubbing out exceptions and then they were
disappearing. The overhead of checked exceptions was having the opposite effect
of what was intended, something that can happen when you experiment (and I now
believe that checked exceptions were an experiment based on what someone thought
was a good idea, and which I believed was a good idea until recently).
When I started using Python, all the exceptions appeared, none were accidentally
"disappeared." If you want to catch an exception, you can, but you aren't
forced to write reams of code all the time just to be passing the exceptions
around. They go up to where you want to catch them, or they go all the way out
if you forget (and thus they remind you) but they don't vanish, which is the
worst of all possible cases. I now believe that checked exceptions encourage
people to make them vanish. Plus they make much less readable code.
In the end, I think we must realize the experimental nature of checked
exceptions and look at them carefully before assuming that everything about
exceptions in Java is good. I believe that having a single mechanism for
handling errors is excellent, and I believe that using a separate channel (the
exception handling mechanism) for moving the exceptions around is good. But I do
remember one of the early arguments for exception handling in C++ was that it
would allow the programmer to separate the sections of code where you just
wanted to get work done from the sections where you handled errors, and it seems
to me that checked exceptions do not do this; instead, they tend to intrude (a
lot) into your "normal working code" and thus are a step backwards. My
experience with Python exceptions supports this, and unless I get turned around
on this issue I intend to put a lot more RuntimeExceptions into my Java code.
One thing has become very clear to me, especially because of Python: the more
random rules you pile onto the programmer, rules that have nothing to do with
solving the problem at hand, the slower the programmer can produce. And this
does not appear to be a linear factor, but an exponential one.
I've gotten a report or two where people were saying that checked exceptions
were such a problem (getting swallowed) in production code that they wanted to
change the situation. It may be that the majority of programmers out there are
what you might classify as beginners. I've seen this again and again in seminars
-- people with years of programming experience who don't understand some basic
things.
Maybe it's a time thing. I started struggling with the idea of exceptions when
they were introduced at the C++ committee, and after this much time it suddenly
hit me. But I also suspect it comes from using a language that has exceptions,
but not checked exceptions. I think it's the best of both worlds -- if I want to
catch the exception, I can, but I'm not tempted to swallow it just to avoid
writing reams of code. If I don't' want to write around the exceptions, I ignore
them, and if one comes up it gets reported to me during debugging, and I can
decide how to handle it then. I still deal with the exception, but I'm not
forced to write a bunch of code about exceptions all the time.
ExceptionAdapter
Here's a tool that I developed with the help of Heinz Kabutz. It converts any
checked exception into a RuntimeException while preserving all the information
from the checked exception.
import java.io.*;
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; }
}
The original exception is stored in originalException, so you can always recover
it. In addition, its stack trace information is extracted into the stackTrace
string, which will then be printed using the usual printStackTrace() if the
exception gets all the way out to the console. However, you can also put a catch
clause at a higher level in your program to catch an ExceptionAdapter and look
for particular types of exceptions, like this:
catch(ExceptionAdapter ea) {
try {
ea.rethrow();
} catch(IllegalArgumentException e) {
// ...
} catch(FileNotFoundException e) {
// ...
}
// etc.
}
Here, you're still able to catch the specific type of exception but you're not
forced to put in all the exception specifications and try-catch clauses
everywhere between the origin of the exception and the place that it's caught.
An even more importantly, no one writing code is tempted to swallow the
exception and thus erase it. If you forget to catch some exception, it will show
up at the top level. If you want to catch exceptions somewhere in between, you
can.
Or, since originalException is public, you can also use RTTI to look for
particular types of exceptions.
Here's some test code, just to make sure it works (not the way I suggest using
it, however):
public class ExceptionAdapterTest {
public static void main(String[] args) {
try {
try {
throw new java.io.FileNotFoundException("Bla");
} catch(Exception ex) {
ex.printStackTrace();
throw new ExceptionAdapter(ex);
}
} catch(RuntimeException e) {
e.printStackTrace();
}
System.out.println("That's all!");
}
}
By using this tool you can get the benefits of the unchecked exception approach
(less code, cleaner code) without losing the core of the information about the
exception.
If you were writing code where you wanted to throw a particular type of checked
exception, you could use (or modify, if it isn't already possible) the
ExceptionAdapter like this:
if(futzedUp)
throw new ExceptionAdapter(new CloneNotSupportedException());
This means you can easily use all the exceptions in their original role, but
with unchecked-style coding.
Kevlin Henney writes:
I must admit that I've come to similar conclusions myself
based on experience in Java and C++, and readings in other languages. My
only caveat on it is that I have found exception specifications, in the
style of CORBA, to be useful where errors are propagated across
significant boundaries, ie machine boundaries, and where such interfaces
need to be more explicit. However, although apparently similar to Java's
EH mechanism, there is no concept of compile-time checking.
To clarify a couple of points in your article, Ada does not have any
form of exception specification, and so does not have a
checked/unchecked model. The roots of exception specifications go back
to CLU and further. The key paper for this is "Exception Handling in
CLU" by Barbara Liskov and Alan Snyder (IEEE Transactions on Software
Engineering, Vol SE-5, No 6, Nov 1979). Unfortunately I cannot find a
copy of this paper online, and have only a paper copy. There is a
"History of CLU" paper
here.
Unfortunately, it does not provide much detail on the exception handling
mechanism. What is worth noting is that information-rich exceptions are
seen as an extension of the procedural paradigm (and are therefore not
necessarily an object-related concept), that CLU supported the
equivalent of throw specs in its procedure signatures, that there were
no compile-time checks of procedure body against the signature (a
conscious design decision to avoid overwhelming programmers with
irrelevant detail :->), and that unlisted exceptions automatically
translated to a special failure exception.
In this you can see the origins of C++'s mechanisms, and the screws that
were tightened -- interestingly rejecting the original rationale for not
having compile-time checks -- in Java. The CLU mechanism has been
influential elsewhere, perhaps the closest offspring being in Modula-3 -
- the language report is available
here
Interestingly, Modula-3 also chooses to have a throw spec that is
runtime rather than compile-time checked.
Kevlin Henney
http://www.curbralan.com
Here's an interesting comment by one of the C# language designers:
(Full comment). Note in particular:
Examination of small programs leads to the conclusion that
requiring exception specifications could both enhance developer productivity
and enhance code quality, but experience with large software projects
suggests a different result -- decreased productivity and little or no
increase in code quality.
The rest of the note goes on to argue this claim. The reason I find this particularly
compelling is that it does agree that checked exceptions seem to be helpful for small projects,
which is generally the space where we argue the point. However, when projects get large (actually,
I've noticed it when they are anything except small), checked exceptions get ungainly and seem
to cause problems. I would therefore suggest that the reason checked exceptions seem so
compellingly "right" at first is that they have been presented and argued in the realm of
small examples.