It must be approaching twenty years since I visited the computer science department of the local university and on one of the notice board spotted a chart for the next decade of development in the industry. The feature of this that I remember was that every odd year predicted "the end of Fortran" and every even year "the end of Cobol". As the author (and I) expected, these languages are still thriving far beyond the end of that chart.
While to work in the software industry is to be exposed to constant change there is much that is constant in spite of that change. While we are regularly exposed to new tools (technologies, languages, techniques, etc) our existing knowledge and tools remain stubbornly useful. This is especially true when we distinguish the fundamental ideas from work-arounds for a specific tool.
For some years I've been very happy with the use of exceptions in C++ programs [Griffiths1999]. Recently I accepted a position at a company working primarily in Java, and consequently had to address the problems being encountered by developers using this language.
Before I go on to describe the problems encountered by my new colleagues, let me revisit the "received wisdom" of the Java development community. This is represented clearly by the following quote from "Thinking in Java" [Eckel2000] (similar views are expressed by other sources):
Keep in mind that you can only ignore RuntimeExceptions in your coding, since all other handling is carefully enforced by the compiler. The reasoning is that a RuntimeException represents a programming error:
An error you cannot catch (receiving a null reference handed to your method by a client programmer, for example).
An error that you, as a programmer, should have checked for in your code (such as ArrayIndexOutOfBoundsException where you should have paid attention to the size of the array).
It is sensible to use RuntimeExceptions to report programming errors - the availability of a call-stack aids reporting them meaningfully. The fact that they can be handled far up the call stack makes implementing an application-wide policy for handling them easier than trying to do so at every point an error is detected. (Examples of policies from different types of application domain are: to abort the current operation, to restart the subsystem, or to terminate the process.)
However, the "received wisdom" very clearly directs a developer towards using ordinary, checked exceptions (i.e. those not derived from RuntimeException) in the design of an application. The use of RuntimeExceptions is discouraged: "programming errors" do occur; but - beyond having a policy for dealing with them when detected - we should not be creating a design to cater for them! (Trying to cater for bugs only leads to hard to test code that is, itself, a breeding ground for bugs.)
The developers that I joined had attempted to apply this guidance and run into a number of problems. However, because of pressure to "just write the code" no attempt had been made to formulate a workable design policy. Letting developers struggle on independently can waste a lot of time over the course of a project - so I called a meeting of the programmers on the team I was working with to discuss the problems they were having with exceptions.
We'll be examining more of the problems they described in the rest of this article - this section deals only with those that bear directly on the above theory (that checked exceptions should be used for everything that isn't a programming error). Before proceeding, I want to make it clear that this development group isn't the only one to experience these problems. They are in good company - as I confirmed at the ACCU Spring conference last year. It seems that this theory doesn't work in practice.
The relevant problems described fall into three categories.
Consider the example of a factory method that is responsible for creating objects. From the point of view of the client code there is no obvious reason why it should fail - so the interface doesn't have a throws clause. From the point of view of an implementation that retrieves objects from a database it is necessary to handle SQLExceptions. For the sake of this discussion let us assume that these reflect something catastrophic - like connectivity to the database being lost.
The SQLExceptions we are considering are not the result of programming errors. Accordingly, we are exhorted not to propagate them as unchecked exceptions. On the other hand we cannot handle them locally (except by the unhelpful expedient of returning a null reference). This leaves two options: either adding a throws clause to allow the factory method to propagate an SQLException or throwing our own exception (normally one that "wraps" the original exception).
Either approach places a burden on the client code - which will generally be in no better position to handle the error than the factory method itself. (Clearly, this is an iterative argument; but, somewhere far up the call stack there will be some code that manages the error.)
The strategy of "wrapping" exceptions prior to propagating them, alluded to in the last section, has the unfortunate side effect of making it difficult to detect the distinction between different problems programmatically. Essentially, one ends up with the situation that all that the programmer can be sure of is that "something went horribly wrong". Admittedly, that is often enough but it occasionally limits options. For example, how can one decide if is it worth retrying the operation that failed?
The alternative strategy (of allowing the original exceptions to propagate) can lead to a similar loss of information. Writing multiple, nearly identical, catch blocks is tedious and a potential source of the familiar maintenance issues caused by "cut and paste". Sooner or later, someone just writes "catch (Exceptions e)" - forgetting the (usually unintended) side effect that this also catches RuntimeExceptions.
Depending upon whether exceptions are allowed to propagate or are "wrapped" two things can happen. Either, an increasing and incoherent set of exceptions begin to appear in the throws clauses of methods dependent on others - until developers get fed up with this insanity and introduce "throws Exception". Or, nearly every method has code to catch exceptions propagated from the methods it depends upon, "wrap" them in a new exception that makes sense within its interface and throw the new exception.
The theory sounds bad enough but, in practice, there is another thing that happens (although no developer admits to doing this intentionally). That is to consume an inconvenient exception by catching and ignoring it.
If these problems that occur in practice are not enough to justify considering alternatives there is a further difficulty: you may be coding to a function signature that doesn't allow you to throw any checked exceptions. This can happen when you are implementing an interface that you don't control (for example Comparator).
In C++ I use exceptions to report problems that it is unreasonable to expect the immediate client code to deal with. In particular, the example of the factory function described above seems to satisfy these criteria: The part of the system that knows how to deal with loss of the database connection is likely to be many layers away from a factory function that retrieves objects from a database.
The problems related in the last section suggest that this approach isn't working - so either the approach is wrong or it is being implemented incorrectly. There are many differences between C++ and Java but there are also lots of similarities between them. Specifically, there are enough similarities in the exception handling mechanisms that I'd expect to use Java Exceptions for the same things that I'd use C++ exceptions for.
What the problems identified above illustrate is the ways in which Java checked exceptions are not like C++ exceptions. Declaring a checked exception places an obligation on the caller of a method to do something explicit with the exception - and that is precisely what isn't desired. What is desired is to transfer program flow in an orderly manner to some point far up the call stack.
There is something in Java that looks far more like my familiar C++ exceptions than Java's checked exceptions: Java's unchecked exceptions. To me they looked like the answer - it doesn't take much thought to conclude that approaching the above scenario by wrapping the SQLExceptions in an unchecked exception causes none of the above problems.
I outlined this approach at the meeting, and there was general consensus that it made sense, but a number of concerns were expressed: mostly regarding the choice that exists between the two exception-handling mechanisms. One of the senior team members agreed to use his notes from the meeting to draft a guideline "exceptions strategy" paper. This would be reviewed at a subsequent meeting when everyone had had an opportunity to think about it. (It is a good idea to review such solutions a few days later - it is very easy to be seduced by an attractive idea and overlook a killer issue during a brainstorming session.) The current version of this paper is included below.
Later on that day I was approached by one of the more thoughtful team member who was concerned that while he couldn't see anything wrong with what I was suggesting he couldn't find any books - or reference material on the internet - that agreed with it. I'm always pleased to be approached like this as I can be wrong, and much of the value of such meetings is lost if people don't think and research for themselves. In this case, I view this as a reflection of the immaturity of the Java community - the hype surrounding the language often gets in the way of recognising an issue and finding a solution. It took the C++ experts from 1990 (when they were introduced as "experimental" in the ARM [Stroustrup1990]) to 1997 (when the C++ standard library was revised to specify its behaviour in the presence of exceptions) to get a consensus on how to use exceptions.
I knew from my experience with C++ that there are ways to write code that works in the absence of the compiler prompting the developer to deal with exceptions. Indeed, I knew the fundamental ideas used in managing exceptions in C++ could also be applied to Java: I presented a translation of them at the ACCU conference 2001 [Griffiths2001]. (Anyway, it isn't the first time I've disagreed with authority - and it surely won't be the last!)
There was one more significant problem that had been observed in a number of existing systems. In these, it had been found to be difficult to handle the errors reported via exceptions effectively. This was believed to be a result of every type of failure reported being reported using the same exception type - albeit with different message text. (If this sounds to you like the java.sql package and ubiquitous SQLException then you won't be surprised that this was mentioned.) The problem was actually worse than with the java.sql package since many parts of the system delivering differing types of functionality threw this same exception, and there was no equivalent of the well established (and fixed) set of SQLState values to deal with.
To address this we concluded that there needed to be guidance covering the choice of the specific exception to use. By requiring any exceptions specified in throws specifications to belong to the package that propagates them we hoped to discourage this habit. And, by checking conformance to the guidelines as part of the class design review, we encouraged a careful consideration of the contract between client code and implementation.
Following the review meeting the team adopted the suggested policy document. (It was updated to clarify it then and a couple of times later, but has remained close to the original discussion.) It reads as follows:
There is at present no clear-cut policy for Java Exception Handling within any of the current OPUS Java systems. This has caused inconsistencies in the use of Exception Handling and these have resulted in problems.
This document addresses the use of two categories of exceptions: checked exceptions and unchecked exceptions.
Checked exceptions provide a mechanism for ensuring that the caller of a method deals with the issue they report. (Either by explicitly handling the exception, or by propagating it.)
Unchecked exceptions should only be considered for "longdistance" exception propagation. (To enable reporting of fairly catastrophic events within the system.)
To support these options all exceptions raised within the system will be subclasses of either OpusException (which extends Exception) or of OpusRuntimeException (which extends RuntimeException). These provided the facility to wrap exceptions.
It is the responsibility of the Class Designer to identify issues that would result in a checked exception being thrown from a class method. Those reviewing the class design check that this has been done correctly. Exception specifications are not changed during implementation without first seeking agreement that the class design is in error.
Exceptions that propagate from public methods are expected to be of types that belong to the package containing the method.
Within a package there are distinct types of exception for distinct issues.
If a checked exception is thrown (to indicate an operation failure) by a method in one package it is not to be propagated by a calling method in a second package. Instead the exception is caught and "translated". Translation converts the exception into: an appropriate return status for the method, a checked exception appropriate to the calling package or an unchecked exception recognised by the system. (Translation to another exception type frequently involves "wrapping".)
Empty catch-blocks are not used to "eat" or ignore exceptions. In the rare cases where ignoring an exception is correct the empty statement block contains a comment that makes the reasoning behind ignoring the exception clear.
This policy seems to have worked well both in terms of being followed without much difficulty by the original team members and for induction of new team members. Subsequently, other teams have also adopted it. To that extent it has been a success. However, it isn't the end of problems with the use of exceptions.
The remaining problems are much more manageable and fall into two categories:]
Catching exceptions at too low a level - rather than allowing them to propagate, they are caught by a piece of code that doesn't have sufficient context to deal with them effectively; and,
Catching too general a range of exceptions - for example, rather than catching OpusException and SQLException separately, and handling each, there is a single catch clause for Exception that then uses instanceOf to identify the exception type. Sometimes such code fails to take account of the possibility of RuntimeExceptions - and "eats" them.
These problems are not as widespread as those reported originally - indeed the majority of developers on the project are unaware of them. Both of these issues reflect poor coding technique and can be addressed by educating developers. This education could have occurred seamlessly as part of the project had we instituted code reviews; but I chose to postpone introducing them in favour of other process changes.
Java developers are rightly encouraged to use unchecked exceptions with caution. However, the current wisdom is too extreme. Unchecked exceptions in Java correspond to exceptions in C++, Smalltalk and C# - and shold be used in the same way: sparingly.
It seems that I'm not the only one to have doubts about this. Shortly after I wrote the first draft to this article Kevlin Henney pointed out that Bruce Eckel had opened discussion on the same point [Eckel].
[Eckel] Bruce Eckel, "Does Java need Checked Exceptions?", http://www.mindview.net/Etc/Discussions/CheckedExceptions
[Griffiths1999] Alan Griffiths, "Here be Dragons", Overload 40 or http://www.octopull.demon.co.uk/c++/dragons/
Overload Journal #48 - Apr 2002 + Programming Topics
|Browse in :||
All > Topics > Programming (768)
Any of these categories - All of these categories