Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / programming / exceptions

Java: Checked Exceptions, Revisited (or, a closer examination of a flawed mechanism)

4.90/5 (3 votes)
6 Sep 2013CPOL10 min read 21.4K  
This post is a re-examination of some topics I discuss in an older post.

This post is a re-examination of some topics I discuss in an older post. This one got long, so I broke into two. I am also going to provide some navigation links:

The Enthusiasm of Discovery

Almost exactly a year ago, I had completed an initial exploration of the Java language, and written a post here in this space about a feature I found attractive in the exception handling mechanism defined by the language. Specifically, I found checked exceptions and the “check or specify” policy associated with them of interest.

That post is here, enthusiastically titled . Ok, perhaps I got a little carried away with the title. Also, in my enthusiasm, I had not yet learned enough to authoritatively comment on the subject. Lastly, I confused the entire Checked Exception paradigm with the small piece of it which I found of interest. More on that in a moment. 

Reddit and /r/java: Brutal and Enlightening 

In the interest of getting some feedback on my observations, I posted a link here in  /r/java of Reddit. Apparently, in my naiveté, I had touched on a sore point in the Java community. The very first comment on the post was a good-natured “Well I see a religious war starting here fairly soon.”

“Well I see a religious war starting here fairly soon.”

                                                              -Reddit commentor

Soon after this first provocative (but humorous and good-natured) comment, there flowed a wealth of fascinating discussion, as experienced Java devs weighed in, both pro and con on the subject of checked exceptions, and provided a plethora of useful context and information. In considering my responses to some of these, I realized that I had missed my mark in my original article (or, the discussion helped me find it, anyway!).  

One very, very helpful tidbit was a link to describing what some of the original thinking was around the checked exception mechanism. This article establishes on paper, at least, an attractive design philosophy, and example cases for the use of checked exceptions.

Cases Made Against the Existing Java Checked Exception Mechanism

Among the discussion points in the Reddit string, the following stand out to me as strong arguments, not necessarily against the Checked Exception architecture itself, but more against the manner it which it has often come to be used in the field:

  1. Many Java libraries and frameworks (especially/including core Java API’s) don’t use checked exceptions in accordance with the original design philosophy.
  2. Checked Exceptions are often used in cases which represent programmer error, rather than predictable events which are at times unavoidable, such as network timeouts, or a storage device is damaged or not available.
  3. Implementation of an existing interface can be problematic if the implementation code requires a checked exception be thrown, and the method definition on the interface does not throw the correct exception.
  4. Using Checked Exceptions is often equivalent to “passing the buck” to the consumer of your Method, class, or API.
  5. The reasoning and actual implementation decisions regarding checked and unchecked exceptions are inconsistent, and no clear rules are available.

Cases Made in Favor of the Checked Exception Mechanism

The following points were made in support of the Checked Exception notion. Philosophically, I agree with them all. It sounds like, though, that points 2 and 3 above may come into play more often than they should in actual practice and negate some of these philosophically sound points:

  1. When methods declare their exceptions, it forces the designer of an API to carefully consider what can go wrong and throw the appropriate exceptions.
  2. Exceptions, declared as part of a method signature, forces the consumer of an API to anticipate and handle what may go wrong.
  3. A method signature represents a contract between the caller and the implementer. It defines the arguments required, the return type, and in the case of Java Checked Exceptions, makes the error cases visible. Requiring a consumer to address anticipatable exceptions theoretically should result in a more robust API (points 2 and three from the previous section notwithstanding).

Clearly, there are some good points on either side of this argument, and like in politics, I find myself stuck on the fence. We will examine my thoughts on this in a bit, after we walk through my current understanding of how things were supposed to be.

On Faults and Contingencies - The Way it was Supposed to Be

According to the article referenced in the link above, a central notion to effective usage of the Java exception architecture is the differentiation between Faults and Contingencies:

Contingency: 
An expected condition demanding an alternative response from a method that can be expressed in terms of the method's intended purpose. The caller of the method expects these kinds of conditions and has a strategy for coping with them.
Fault:

An unplanned condition that prevents a method from achieving its intended purpose that cannot be described without reference to the method's internal implementation.

The article provides a simple example case in which an API defines a processCheck() method. processCheck will either process a check as requested by the client code, or throw one of two exceptions related to the problem domain: StopPaymentException or InsufficientFundsException. These are presented as examples of Contingencies for which any client calling upon this method should be prepared, and therefore, as exemplary models for Checked Exception usage.

The article additionally discusses a third possibility, in which database access, as part of the transaction processing performed by processCheck, utilizes the JDBC API. JDBC throws a single checked exception, SQLException, to report problems with accessing the data store. Therefore, our processCheck method is required to handle SQLException, or pass any such occurrence up the call stack by including SQLException in its throws clause. In this last case, client code is unlikely to have sufficient context to deal appropriately with whatever caused the SQLException (nor, for that matter, is the processCheck method itself) other than gracefully exiting and informing the calling procedure that something went wrong while accessing the database. This last case is an example of a fault.

To my way of thinking, Contingencies usually exist within the problem domain, and in fact client code calling upon an API is more likely to contain an effective strategy for dealing with them than is the API itself. Faults, on the other hand, represent unexpected conditions which, when our equipment and program is working as designed, should not occur at all. Note that I include “program working as designed” in that sentence. Programmer error and code bugs, for me, fall into this category.

A Overly Simple, Hacked Example

So, thinking I have absorbed the thinking put forth in the article, I construct a hasty example structure which extends the example in the article into a bit of pseudo-code. Note, I am not representing this to be good code, and it represents a hack design at best, greatly over-simplified. My objective is to illustrate the exception usage concepts under discussion. First is the CheckingAccountClass:

A silly mockup of the CheckingAccount Class:
public class CheckingAccount 
{
    private String _accountID;
    private double _currentBalance;
    private ArrayList<Integer> _stoppedCheckNumbers;
    
    public String getAccountID()
    {
        return _accountID;
    }
    
    
    public double getCurrentBalance()
    {
        return _currentBalance;
    }
    
    
    public void setAccountID(String accountID)
    {
        _accountID = accountID;
    }
    
    
    public void setCurrentBalance(double currentBalance)
    {
        _currentBalance = currentBalance;
    }
    
    
    public ArrayList<Integer> getStoppedCheckNumbers()
    {
        if(_stoppedCheckNumbers == null)
        {
            _stoppedCheckNumbers = new ArrayList<Integer>();
        }
        
        return _stoppedCheckNumbers;
    }
    
    
    public double processCheck(Check submitted) 
    throws InsufficientFundsException, StopPaymentException, 
    DatabaseAccessException
    {
        if(_stoppedCheckNumbers.contains(submitted.getCheckNo()))
        {
            throw new StopPaymentException();
        }
        
        double newBalance = _currentBalance - submitted.getAmount();
        
        if(newBalance < 0)
        {
            throw new InsufficientFundsException(_currentBalance, 
                    submitted.getAmount(), 
                    newBalance);
        }
        
        try
        {
            // <Code to Update Database to reflect current transaction>
        }
        catch(SQLException e)
        {
            // <Code to log SQLException details>
            
            /*
             * After logging and/or otherwise handling the SQL failure, throw
             * an exception more appropriate to the context of the client code, 
             * which does not care about the details of the data access operation, 
             * only that the information could not be retrieved. 
             */
            throw new DatabaseAccessException("Database Error");
        }
        
        return newBalance;
    }
}

In order to examine this in context, we will also want to look at a usage scenario with some mock client code. In order to do THAT, we also need a Bank class, which:

  1. Provides a static factory method to access CheckingAccount objects, and;
  2. Is also subject to the nefarious SQLException while doing so.
  3. Introduces yet another contingency - what if the account number submitted on a check does not exist? For this eventuality, we define a fourth Contingency Exception: InvalidAccountException.
The Bank Class (with pseudo-code):
public class Bank 
{
    public static CheckingAccount getCheckingAccount(String AccountID) 
    throws DatabaseAccessException, InvalidAccountException
    {
        CheckingAccount account = new CheckingAccount();
        
        try
        {
            /*
             * <Code to retrieve Account data from data store>
             */
            
            if(//no row is returned for AccountID)
            {
                throw new InvalidAccountException();
            }
                    
            // Use test data to initialize an account instance:
            account.setAccountID("0001 1234 5678");
            account.setCurrentBalance(500.25);
            account.getStoppedCheckNumbers().add(1000);
        
        }
        catch(SQLException e)
        {
            // <Code to log SQLException details>
            
            /*
             * After logging and/or otherwise handling the SQL failure, throw
             * an exception more appropriate to the context of the client code, 
             * which does not care about the details of the data access operation, 
             * only that the information could not be retrieved. 
             */
            throw new DatabaseAccessException("Database Error");
        }
        
        return account;
    }
}

The Bank class above provides the functionality needed to mock up some client code. I am not going to get all fancy with this, and the overall class structure is not what I am here to examine. We will pretend that the void main method used here actually represents some code in the service of a user interface, and see what our Checked Exception-heavy design looks like from the consumption standpoint:

Some Mock Client Code Consuming the Bank and CheckingAccount API’s:
Java
public class MockClientCode 
{
    /*
     * Assume this code is supporting UI operations.
     */
    
    static String SYSTEM_ERROR_MSG_UI = ""
        + "The requested account is unavailable due to a system error. "
        + "Please try again later.";
    
    static String INVALID_ACCOUNT_MSG_UI = ""
        + "The account number provided is invalid. Please try again.";
    
    static String INSUFFICIENT_FUNDS_MSG_UI = ""
        + "There are insufficient funds in the account to process this check.";
    
    static String STOP_PAYMENT_MSG_UI = ""
        + "There is a stop payment order on the check submitted. 
        + "The transaction cannot be processed";
    public static void main(String args[])
    {
        // Sample Data:
        String accountID = "0001 1234 5678";
        int checkNo = 1000;
        double checkAmount = 100.00;
        
        // Use test data to initialize a test check instance:
        Check customerCheck = new Check(accountID, checkNo, checkAmount);
        
        CheckingAccount customerAccount = null;
        double newBalance;
        
        try 
        {
            customerAccount = Bank.getCheckingAccount(customerCheck.getAccountID());
            newBalance = customerAccount.processCheck(customerCheck);
            
            // Output transaction result to UI:
            System.out.printf("The transaction has been processed. New Balance is: " 
                    + DecimalFormat.getCurrencyInstance().format(newBalance));
        } 
        catch (DatabaseAccessException e) 
        {
            // Output the message to the user interface:
            System.out.println(SYSTEM_ERROR_MSG_UI);
        } 
        catch (InvalidAccountException e) 
        {
            // Output the message to the user interface:
            System.out.println(INVALID_ACCOUNT_MSG_UI);
        } 
        catch (InsufficientFundsException e) 
        {
            // Output the message to the user interface:
            System.out.println(INSUFFICIENT_FUNDS_MSG_UI);
        } 
        catch (StopPaymentException e) 
        {
            // TODO Auto-generated catch block
            System.out.println(STOP_PAYMENT_MSG_UI);
        } 
    }
}

As With Most Things, a Series of Trade-offs

The “design” above (I am using the term very loosely here) works in accordance with the Java Checked Exception mechanism, and specifically addresses most of the concerns discussed in the Oracle article. I included my own embellishment at the point where the SQLException is potentially thrown locally, by way of logging the data-access specifics for examination by the dev team and/or the dba, and throwing a more general contingency-type exception defined for the problem space (DatabaseAccessException) which can then be handled by client code in a proper context (“Sorry, we seem to be experiencing a system outage. Please try again later”).

Upsides:

On the upside, this code makes robust use of the Check-or-Specify policy built in to the Java environment. Code which attempts to call the public processCheck method will be required to handle all three contingency cases:

  1. The account number submitted on the check is not valid within the system
  2. There is a Stop Payment order on the check number being processed
  3. There are not sufficient funds available to cover the withdrawal
  4. There is a problem accessing account data (for whatever reason)

From an API design standpoint, this could be considered a good thing. Developers who may be utilizing this class as part of a library or framework will know immediately what contingencies they will have to address within their own code.

With respect to the SQLException (a fault, as opposed to a domain contingency), there is very little that can be done about this even at the point where it is first thrown, other than log the details and notify the calling method that there was a problem retrieving the requested data. In my mind, the farther this specific exception is allowed to propagate from its source, the there is even less ability to do anything with the information it contains. So in my example, I use the try . . . catch block to deal with it as best we can, and then propagate a more appropriate contingency exception.

Also, while the wealth of business-logic-related exceptions is a bit strange looking from my C# background, the code in the mocked up client class is actually pretty easy to read, and figure out what is going on.

Downsides:

On the other hand, what should be a (relatively) simple class structure actually introduces a total of FIVE new types into our project:

  1. The CheckingAccount class itself
  2. The Bank class
  3. Four new exception types:
    1. InvalidAccountException
    2. StopPaymentException
    3. InsufficientFundsException
    4. DatabaseAccessException

Client code attempting to use our CheckingAccount class must be aware of all four types, which increases the number of dependencies within a client class. While not serious, as a project grows larger, the inflated number of new types created as contingency exceptions may grow large, and the number of dependencies may grow proportionately. All in all, this could substantially increase project complexity. As one Reddit commenter pointed out, one of the problems with implementations of the Checked Exception mechanism in Java is that it results in what is essentially a “shadow type system.” Hard not to disagree, in light of the example Oracle provides us with.

On top of this, the excessive number of catch clauses is almost as bad as introducing a big switch statement.

Also, within the Oracle article, and within this class definition, what we are essentially doing through the use of “contingency exceptions” is using the exception mechanism to address business logic concerns.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)