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:
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 Things to love about Java: Exception Handling. 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 this article on the Oracle website 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.
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:
- Many Java libraries and frameworks (especially/including core Java API’s) don’t use checked exceptions in accordance with the original design philosophy.
- 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.
- 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.
- Using Checked Exceptions is often equivalent to “passing the buck” to the consumer of your Method, class, or API.
- The reasoning and actual implementation decisions regarding checked and unchecked exceptions are inconsistent, and no clear rules are available.
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:
- When methods declare their exceptions, it forces the designer of an API to carefully consider what can go wrong and throw the appropriate exceptions.
- Exceptions, declared as part of a method signature, forces the consumer of an API to anticipate and handle what may go wrong.
- 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.
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.
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
{
}
catch(SQLException e)
{
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:
- Provides a static factory method to access
CheckingAccount
objects, and; - Is also subject to the nefarious
SQLException
while doing so. - 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
{
if(
{
throw new InvalidAccountException();
}
account.setAccountID("0001 1234 5678");
account.setCurrentBalance(500.25);
account.getStoppedCheckNumbers().add(1000);
}
catch(SQLException e)
{
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:
public class MockClientCode
{
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);
}
}
}
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:
- The account number submitted on the check is not valid within the system
- There is a Stop Payment order on the check number being processed
- There are not sufficient funds available to cover the withdrawal
- 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:
- The
CheckingAccount
class itself - The
Bank
class - Four new exception types:
InvalidAccountException
StopPaymentException
InsufficientFundsException
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.