Exception Handling in Java: Contingencies vs. Faults
When and how to use checked and unchecked exceptions
Join the DZone community and get the full member experience.
Join For FreeIntroduction
Since the invention of the Java language, there has been a long-standing debate about checked versus unchecked/runtime exceptions. Some people argue that checked exceptions promote a better design. Others feel that checked exceptions get in the way, especially as systems mature and refactor over time, and therefore unchecked exceptions are better. The Effective Java Exceptions article settles this debate once and for all: both checked and unchecked exceptions are acceptable, and each has its purpose within an application. I highly recommend reading that article. I will refer back to its concepts and terminology going forward.
Exception Handling
Exception handling is an important part of the design and architecture of an application. It represents alternate flows or scenarios other than the "happy path" flow through an application. Generally, business requirements only tell you how the application should behave when everything happens as expected. They rarely describe what to do when something goes wrong.
Java got it right conceptually by allowing for both checked and unchecked exceptions. The problem is that developers have been taught poorly, or not taught at all, how to use one over the other properly. Even examples within the language itself are poorly designed and flawed. Think about classes in the java.sql
or java.io
packages. The majority of methods in classes in those packages throw SQLException
or IOException
that the caller must catch and handle.
Other languages, such as Ruby, Kotlin, Scala, and Groovy, as well as other Java frameworks, such as Spring, swung the pendulum to the opposite side of the spectrum because of this. These languages don't have the concept of checked exceptions and Spring Framework treats most things as unchecked. This mentality leaves things up to the developer building the application to know what exceptions to catch in certain situations.
Contingencies and Faults
The article mentioned above introduces two main concepts: contingencies and faults. Table 1 defines these concepts and is taken from the article.
Table 1: Contingencies and Faults
cONdition | contingency | fault |
---|---|---|
Is considered to be | A part of the design | A nasty surprise |
Is expected to happen | Regularly but rarely | Never |
Who cares about it | The upstream code that invokes the method or the user using the system | The people who need to fix the problem (i.e., network admin, DBA, etc.) |
Examples | Alternative return modes | Programming bugs, hardware malfunctions, configuration mistakes, missing files, unavailable servers, etc |
Best Mapping | A checked exception | An unchecked exception |
Example
Let's show an example using this information: I want to transfer money from one account to another. Somewhere in my application, I have a method called transferFunds
. This method needs three pieces of information:
- The account to transfer funds from (the from account).
- The account to transfer funds to (the to account).
- The amount to transfer
What are some possible outcomes that could happen during this transaction?
- The transfer completed successfully.
- The user tried to transfer more money than they had in the from account.
- The network, database, or any downstream services failed during the transaction.
Outcome #1 is the "happy path" flow through the transfer funds operation, so there isn't any exception handling to be done. What about the other two outcomes? These outcomes result in an alternate flow where someone has to do something.
Let's apply the rules in Table 1 to the other outcomes:
- Outcome #2
- Is considered to be?
- A part of the design
- The application needs to provide a good user experience if a user tries to transfer more money than they currently have.
- A part of the design
- Is expected to happen?
- Regularly but rarely
- There will be users who will try to transfer more money than they have in their accounts. Not all instances of this are considered fraud. The user could simply have mistyped or put a decimal point in the wrong place.
- Regularly but rarely
- Who cares about it?
- The user using the application
- Paging a DBA because the user doesn't have enough money to transfer isn't a valid solution. A DBA isn't going to give the user additional funds to complete the transfer.
- Instead, the application needs to inform the user about the situation and allow them to fix it.
- The user using the application
- Best mapping?
- A checked exception
- Is considered to be?
- Outcome #3
- Is considered to be?
- A nasty surprise
- We never expect the network, database, or downstream services to be unavailable.
- A nasty surprise
- Is expected to happen?
- Never
- Who cares about it?
- A DBA, network administrator, or another internal resource who can troubleshoot the failure
- Telling the end-user that the network is down or some query failed to execute isn't going to help. There is nothing the end-user can do about it.
- The end-user should get an error saying something like, "I'm sorry, but we're currently experiencing technical difficulties. Our support team is currently investigating the cause of this issue. Please try your transaction again later."
- A DBA, network administrator, or another internal resource who can troubleshoot the failure
- Best mapping?
- An unchecked exception
- Is considered to be?
Mapping to Method Signatures
Given the above information, what do you think a method signature for our transfer funds operation should look like?
public void transferFunds(Account fromAccount, Account toAccount, BigDecimal amount) throws InsufficientFundsException
This method signature is very explicit and self-documenting. A developer calling this method, without having to read any documentation, understands that the method is going to do one of the following things:
- Complete successfully (Outcome #1)
- User tried to transfer more funds than available (Outcome #2)
In fact, because the method declares to throw a checked exception, the developer calling this method will be forced by the compiler to deal with it appropriately. Generally, contingencies are dealt with by the immediate caller.
In the case of Outcome #3, there is nothing the developer needs to do or care about. The fault barrier will catch it, log it, and respond to the user with an appropriate message.
Another question I like to ask is "Is Outcome #2 an error condition?" Some developers might say "yes" because the method threw an exception therefore an error has occurred. However, it is not an error condition. Contingencies are not error conditions; they are alternate, yet expected, flows through an application. The application would never log an error or stack trace in the case of InsufficientFundsException
. The problem is that developers are trained that when they see the word "Exception" in a class name or throws
in a method signature they immediately assume error. In such cases, we could even rename InsufficientFundsException
to InsufficientFundsContingency
to make it more explicit.
Others might argue that the same use case could be implemented without the checked exception. They may propose a method signature like
xxxxxxxxxx
public boolean transferFunds(Account fromAccount, Account toAccount, BigDecimal amount)
or
xxxxxxxxxx
public BigDecimal transferFunds(Account fromAccount, Account toAccount, BigDecimal amount)
or
public void transferFunds(Account fromAccount, Account toAccount, BigDecimal amount)
In each of these first two cases, the return type is trying to be re-used for the implementation of the alternate scenarios. In theory, this would probably work. The first signature may return true
or false
as to whether or not the transaction was successful. If it were not successful, though, there would be no way to determine or communicate why.
The second signature might return the current balance in the from account if the transaction was not successful. This may be a good thing to communicate to the end-user, but it only allows this one specific use case of not enough funds to be transferred.
The third signature looks a lot like the one proposed just without the throws
clause. In this case, there may be unchecked exceptions being used to convey various alternate conditions that could potentially occur.
Hopefully, in all cases, the developer writing the methods was very explicit in the Javadocs about the behavior of the method and the conditions of the return type. Additionally, it would be imperative that the documentation has been kept up-to-date over time as the application has evolved.
Adding Additional Use Cases
What if an additional requirement was added later: "If the from account is jointly-owned, both owners have to enter their credentials to perform the transaction." How would you capture that in any of the three alternate method signatures proposed? It would be easy in the original method signature using contingencies:
xxxxxxxxxx
public void transferFunds(Account fromAccount, Account toAccount, BigDecimal amount) throws InsufficientFundsContingency, JointOwnershipCredentialsRequiredContingency
The method's caller can then deal with either contingency. Each contingency requires the end-user to perform some action to correct the situation. In addition, the method signature itself is self-documenting as to the behaviors and potential outcomes of any calls to it.
Domain Modeling
Contingencies are modeled just like any other domain object within an application. Contingencies are entities, just like the Account
object in our example, and should be modeled as such. The only difference is that a contingency has a base class: Exception
. Contingencies can additionally be modeled hierarchically, just like you would with other domain objects. You need to consider your contingencies and be sure to model them when creating your application's domain model.
Contingencies can also contain state. In our example, the InsufficientFundsContingency
class could store the available account balance so that its value can be provided back to the end-user. The caller of the transferFunds
method wouldn't need to make an additional call to find out the available balance. It would be right there on the contingency object.
Gaining Extra Performance
Whenever the JVM instantiates an instance of any class that extends Throwable
, the JVM has to pause, synchronize all of the threads, and construct a stack trace for the Throwable
. We stated previously that a contingency is not an error condition, therefore it will never be logged nor its stack trace printed.
There are two main ways in which this can be avoided. In each case, the end result is the same. The decision on which implementation to use mostly depends on what you want your contingency class to look like and about personal and/or your team's or organization's taste.
Override protected constructor
The Throwable
class contains a protected
constructor:
xxxxxxxxxx
protected Throwable(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace)
A contingency class can create its own constructors and call this super constructor with
xxxxxxxxxx
public MyContingency() {
super(null, null, false, false);
}
The contingency class in this case may or may not have a message to pass to the super constructor. Remember, contingencies are not error conditions and therefore may not have a message. Any state stored in the contingency class would be managed by the contingency class itself.
Override fillInStackTrace method
The fillInStackTrace
method can be overridden in contingency classes to improve performance and remove the needed thread synchronization and stack trace computation:
xxxxxxxxxx
public Throwable fillInStackTrace() {
// This method is being overridden as a performance improvement.
// This exception class is a contingency & as such will never denote
// an error condition nor will its stack trace ever be
// looked at/printed to a log/etc.
//
// Whenever an exception class is instantiated, this method is called
// from the constructor.
//
// Throwable.fillInStackTrace() is also marked as synchronized,
// meaning whenever an Exception instance is created, the JVM
// has to be paused so the complete stack trace can be computed.
//
// Because this is a contingency, we will never need the stack trace.
// Overriding this method we also remove the "synchronized" keyword
// from the method signature. This will boost performance a bit
return this;
}
Opinions expressed by DZone contributors are their own.
Comments