Exceptional Strategies
Microsoft's C# language does not include an equivalent of Java's checked exceptions. Does this encourage lazy and error-prone
code or is it an astute recognition that Java's checked exceptions are more trouble than they are worth?
Java Exceptions in a Nutshell
The Java language has three categories of exception,
- instances of the java.lang.Exception class and its subclasses
- instances of the java.lang.RuntimeException class and its subclasses
- instances of the java.lang.Error class and its subclasses
The Java compiler treats the first category of exceptions, the so-called checked exceptions, differently from
the latter two, the unchecked exceptions. When a method creates a checked exception object and throws it
(raises it), the compiler expects the method to either catch (handle) that exception later in its code or declare
in its signature that it throws objects of that exception class or one of its superclasses. A compile-time error occurs
if these conditions are not met.
Unchecked exceptions are free from these constraints; they can be thrown anywhere and propagate up the stack of nested
method calls until they are caught by an appropriate catch block or they reach the top of the call stack and
cause the program to terminate.
Conventional Java wisdom teaches that unchecked exceptions should be used for errors in programming logic(Strategy
#2) or when errors occur within the underlying virtual machine or operating environment (Strategy
#3). Checked exceptions should be used for recoverable situations (Strategy #1). For example,
a checked exception should be used when a user has supplied invalid data values so that the user can be asked to supply
valid values instead. Also a checked exception should be used where a time-out may occur and the caller might want
to try again to see if the condition causing the time-out has cleared.
Checks out of Fashion?
Microsoft chose not to include the concept of checked exceptions in C#. As a result there seems to be a growing number
of voices arguing that checked exceptions cause more problems than they solve and are a failed experiment.
When a change in a method's implementation causes that method to throw a new checked exception, developers have a choice.
- Add the exception to the list of classes in the throws clause at the end of the method's signature.
- Catch the exception and instead either throw an unchecked exception(Strategy #8), or a checked
exception that is already declared in the methods signature (Strategy #12).
- catch the exception and either ignore it, or simply log the exception and carry on as if nothing had happened
(Strategy #7).
Option 1 may cause a severe ripple affect because the callers of the method must now catch and handle that exception
or declare that they throw it too. If the calling methods change their signature to include the declaration then their
callers are affected and so on. In a large body of code this could result in small changes in many classes. Object-oriented
programming is all about encapsulation and localizing change; this option would seem to be at odds with this ideal.
Option 2 may hide the real cause of the exception from someone trying to determine why an exception is being raised.
The usual solution to this is to pass the original exception as an argument to the new exception's constructor so
that it can be retrieved by error handling code and detailed exception information is not lost.
Option 3 often means an incorrect result goes unrecognised until it causes a worse problem somewhere else making the
original problem much harder to track down. This is almost universally acknowledged to be a bad practise.
Those arguing against checked exceptions point to the frequent occurrence of developers picking option 3 and suggest
that checked exceptions actively encourage this bad practise because it is the least work to implement. They also
point out that option 2 can tempt developers to create unsuitable inheritance relationships between exceptions to
avoid using option 1.
So is there a case for ridding the world of checked exceptions? In my opinion, the answer has to be, "No".
Firstly, it should be remembered that exceptions should only be used for exceptional circumstances and checked exceptions
should only be used for the subset of those where an application can be reasonably expected to recover (Strategy
#1). This should be a relatively small number of cases.
Secondly, if a method does not list all the exceptions it throws it can be very hard to know exactly what exceptions
are thrown by that method. This is especially true if the source code of the class being called is not available (part
of a third party component, product, or framework). For this very reason, many people recommend listing the unchecked
exceptions thrown by a method even though this is not enforced by the compiler.
Thirdly, the throws clause forms part of the interface of a method and the contract with its callers. Any change to
the interface of a method may cause a ripple affect through its callers. We often make the parameters and return type
of a method more generic than strictly necessary to avoid changing a method signature to frequently. We can follow
the same sort of strategy for exceptions by declaring that they throw a superclass of the exact exception thrown (Strategy
#12). Of course, in Java we are limited to single inheritance; maybe it would be better if the throws clause took
Java interfaces instead of specific classes. It may have also be better if java.lang.Exception and java.lang.RuntimeException
were abstract classes so that the temptation to use them instead of more useful specific subclasses is removed (Strategy
#4).
It may be that checked exceptions have been overused in a similar way to which inheritance was overused when object-oriented
programming first burst into mainstream software development. However, I believe much of the problem with exception
handling is caused by indiscipline and the abandoning checked exceptions will create as many problems as it solves.
For example, a developer adding or changing the type of an unchecked exception thrown deep in a program may cause a
program to unravel all the way to a generic high-level catch block or cause a thread to exit abruptly. For non-recoverable
situations such as programming logic problems this may be the best that can be done but it is not acceptable for simple
recoverable situations such as a time-out on a database or network request, or small problem in a set of data supplied
by a user of the system.
In conclusion, I'd prefer to keep checked exceptions in Java. Exceptions in Java may not be perfect but I like having
the choice of using checked or unchecked exceptions.
Here are a dozen strategies for using exceptions in Java. I'd be happy to receive suggestions for others and to hear
about your views and opinions on the subject in the forums at www.thecoadletter.com or by e-mail. A version of my
original guidelines on exceptions can be found at http://www.nebulon.com for those who enjoy ancient history :-)
Java Exception Strategy #1: Use checked exceptions for recoverable situations
Use a subclass of java.lang.Exception to represent a transient or recoverable problem. The name of the new class should
end with the suffix Exception to distinguish it from business-as-usual classes.
Example: NullParameterRuntimeException - thrown when a parameter of a method is expected to have a non-null
value.
Java Exception Strategy #2: Use unchecked for programming logic problems
Use a subclass of java.lang.RuntimeException to represent a programming problem. The name of the new class should end
with the suffix RuntimeException to make it clear that this exception is not required by the Java compiler to be declared
in the throws clause of method signatures.
Example: NullParameterRuntimeException - thrown when a parameter of a method is expected to have a non-null
value.
Java Exception Strategy #3: Reserve the use of java.lang.Error for underlying virtual machine problems
Under normal application development do not create or throw instances of java.lang.Error or its subclasses. Reserve
these types of exception for problems with the underlying virtual machine.
Example: java.lang.OutOfMemory - the exception thrown when the virtual machine has exhausted its allocated memory
space.
Java Exception Strategy #4: Always throw a subclass of java.lang.Exception or java.lang.RuntimeException.
Do not explicitly create and throw instances of java.lang.RuntimeException or java.lang.Exception. Always throw a more
specific subclass instead. This enables error handling code for specific exceptional conditions to be written easily.
In general, if error handling code has to inspect the contents of an exception object to determine what action to
take then the exceptions being thrown are not specific enough.
Example:
throw new Exception( "Specific message" ); // No !!!
throw new SpecificException(); // Yes !!!
|
Java Exception Strategy #5: Create exception classes to represent what went wrong, not where.
Name exception classes so that they represent the type of exceptional circumstance that took place and not the class,
package, component where the exceptional circumstance took place. Where the exception occurred should be part of the
exception details. This promotes the reuse of exception classes and avoids having multiple different exception classes
representing the same problem in different places.
Example: BalanceTooLowForDebitException and not BankingAccountException.
Java Exception Strategy #6: Use the same parameterized message template for all instances of an exception
class
Objects of the same checked exception class should represent a specific exceptional situation and therefore always
use the same message template. Values within the message may be different but the message template itself should be
the same. The constructor of the exception class should take parameters for the message as arguments and not the message
text itself. An exception class should know how to obtain its own message template text from a class constant, properties
file, or other persistent store. If you find yourself wanting an exception to have a different message in a different
context, create a separate exception class for that context. Having exceptions manage their own message text also
makes listing exception messages for localization, user guides, programming guides, etc., much easier than if the
messages are scattered throughout the whole of the source code.
Example: Instances of BalanceTooLowForDebitException should always use the message template "Account {0}
does not have a high enough balance to process a debit of {1} {2}" where {0} is replaced by the account number,
{1} is replaced by the debit amount, and {2} is replaced by the currency.
Java Exception Strategy #7: Do not catch and ignore exceptions
Only in relatively rare circumstances is it correct to catch and ignore an exception. If an exception cannot be handled
sensibly at that point in the code, the exception should be allowed to propagate up the method call stack until it
can be handled sensibly. Simply displaying an exception's details on a system console or in an error log and then
allowing processing to proceed is not good considered 'handled sensibly'.
Example: challenge code similar to the following:
try { ... }
catch (SomeException e1) // catch any instances of SomeException thrown in the try block
{ } // totally ignore the SomeException objects caught
catch (SomeOtherException e2) // catch any instances of SomeOtherException thrown in the try block
{
e2.printStackTrace(); // log an exception's details but otherwise ignore it
}
|
Java Exception Strategy #8: Do not catch checked exceptions and throw an unchecked exception
It only makes sense to catch a checked exception and immediately throw an unchecked exception if, at that point in
the code, the occurrence of a checked exception indicates a defect in the program logic. Catching a checked exception
and immediately throwing an unchecked exception only to avoid adding to the throws clause of a method signature is
never valid.
Example: challenge code similar to the following:
try { ... }
catch (SomeCheckedException e1) // catch instances of a checked exception
{
// throw an unchecked exception
throw new SomeRuntimeException( e1 );
}
|
Java Exception Strategy #9: Catch unchecked exceptions and throw a checked exception
When a runtime exception has been thrown that the system might be able to recover from sensibly. Catch the runtime
exception and throw a checked exception.
Example: catch a runtime exception representing a network time-out and throw a checked exception so that the
calling methods know that they can catch a time-out and request a retry if it makes sense for them to do so.
Java Exception Strategy #10: Do not propagate exceptions from one architectural layer to another
Catch exceptions thrown by another architectural layer and throw an exception from the current architectural layer.
Always pass the caught exception as an argument to the constructor of the new exception so that detailed information
is not lost. This preserves the separation of concerns that the architecture layers provide.
Example: Catch an SQLException thrown by JDBC code and throw a SaveException that holds the original SQLException
object in an instance variable.
Java Exception Strategy #11: Create abstract superclasses for related sets of exceptions
Regularly review existing exceptions usage and introduce abstract superclasses where the same corrective action might
be used for a set of different exception classes. Callers can then name a single superclass in a catch block instead
of all the individual exception classes for which a corrective action is appropriate. Making the superclasses abstract
continues to enforce the throwing of specific concrete exceptions.
Example: Create an abstract superclass InvalidDebitValueException for exceptions like BalanceTooLowForDebitException
and NegativeDebitValueException.
Java Exception Strategy #12: Consider declaring that a method throws a more generic exception
Before adding a specific exception class to the throws clause of a method, consider adding a superclass of that exception
instead. Closely related to strategy #11, careful thought about what can go wrong in a method's execution and the
sort of corrective action that needs to be taken, can help avoid ripple affects later if the method's implementation
is changed. In the same way that parameters and return types of a method may be declared to be of more general types
than strictly necessary to avoid a change in implementation causing a change in the method's signature, so with exceptions
named in the throws clause.
Example: declare a method that debits an account as throwing an InvalidDebitValueException instead of a BalanceTooLowForDebitException.
Then should the implementation of the account be changed so that only debits upto a certain daily amount are allowed,
a new subclass of InvalidDebitValueException can be introduced, DailyDebitAmountExceededException, without causing
a change to the method's signature.
Helping Developers Apply the Strategies
Create two general abstract classes, one that extends java.lang.Exception and another that extends java.lang.RuntimeException.
Insist that all exceptions thrown be your application code be subclasses of these two new classes. These two classes
can also include the code needed to retrieve the exception's message template from a property file or database, parse
it and insert the parameters supplied. They can also support the nesting of other exceptions within themselves. Doing
this helps developers apply strategies 4,5 and 9.
Well run code inspections or pair programming can help developers apply the other strategies in the list.
Some automated code auditing tools may be able to identify ignored exceptions and the explicit creation of java.lang.Exception,
java.lang.RuntimeException and java.lang.Error instances, or explicit creation of classes ending in 'Exception' that
are not subclasses of your two general abstract exception classes.
For a large project/system it may be worth assigning someone the job of managing exceptions so that existing exceptions
are reused where appropriate and not used where inappropriate.