The Coad Letter: Modeling and Design Edition, Issue 90, Exceptional Strategies

By: Coad Letter Modeling Editor

Abstract: In this issue, we look at exceptions in Java and what makes them so exceptional.

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.

  1. Add the exception to the list of classes in the throws clause at the end of the method's signature.
  2. 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).
  3. 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.


Server Response from: ETNASC02