Exceptional Java – Exception design relativity

Designing the error path in the code is not the most entertaining part of a programmer’s job. We are focused on coding the solution, the success path, and the damn exceptions stand in the way demanding to be handled. What makes it even harder and murkier is the lack of well established rules for what constitutes an exception and how to decide what kind of exception each one is: checked vs unchecked. And this decision is haunting because whatever you decide will influence the handling policy in the client code.

Let’s look at a very simple and trivial example and try to decide what to do exception wise.

In a banking system, simplifying to extreme, we will probably have some kind of Account class. And the interface of the account class might have a transferFunds method that could look like this:

public void transferFunds(long amount, Account destination)
{
    ...
}

This seems really simple and it should be. But what happens in the real world is not always simple. With this simple interface we already can think about lots of problems going wrong. Let’s take one of the possible problems and analyze it: what if the current account doesn’t have enough funds to satisfy the request? In this case our method has to stop the transaction, fail the request and let the caller know about the failure with as much details as possible.

Common Java wisdom gives us a few general principles about how to work with exceptions:

  • 1. Don’t use exceptions for flow control and expected business situations, but only for exceptional situations
  • 2. Use unchecked exceptions for programming errors, bugs, situations where the application or the current operation cannot expect to recover from.
  • 3. Use checked exceptions for situations where the caller is expected to be able to make a full or partial recovery after the failure

In our simple example let’s try to apply this wisdom. But before writing code we ask ourselves a few questions and make a few decisions. Obviously the lack of funds situation has to be treated. But what kind of situation is it?

Solution number one: Normal business scenario, don’t use exceptions

This is obviously a normal business situation that certainly can happen in day to day operations. Based on rule number 1 (above) we might decide that we don’t need exceptions. But we still need to treat the situation so we modify our method this way:

public boolean transferFunds(long amount, Account destination)
{
    if (! this.hasFunds(amount)
    {
        return false;
    }
    ...
    
    return true;
}

Simple enough but with two problems:

  • The caller has to check the return value; if he doesn’t he might continue with a wrong assumption and screw things up later.
  • The caller doesn’t have enough information so he has to make additional calls after the failure to see what went wrong. Keep in mind the transfer can fail for other reasons too and this solution provides no differentiation. We can return a result information object instead of a boolean but it feels like too big a complication.

With this kind of solution we are back to the way C programs were written. Weren’t suppose to deal better with this kind of situations in Java?

Solution number two: A request for funds transfer when no funds are available is a bug

Another way of thinking of this is to say nobody should ask for money that don’t exist. For this solution we are going to implement a new method to check available funds. Also, since we consider the situation a bug, we throw an unchecked exception.

public class NotEnoughFundsException extends RuntimeException
{
    ...
}

...

public boolean hasFunds(long amount)
{
    ... check for funds and return true or false
}

public void transferFunds(long amount, Account destination)
{
    if (! this.hasFunds(amount)
    {
        throw new NotEnoughFundsException();
    }
    ...
}

Of course we have to document our API and specify that whoever wants to transfer funds has to check if they are available and the in the same transaction to do the transfer.
This solution has some advantages:

  • If the client code fails to treat the exception at least the program doesn’t continue on a wrong path.
  • We can send more information about the failure context in the exception class through the exception type and the exception instance content.

But we also introduce problems:

  • The client code programmer is not automatically warned to treat the exception (like in the first solution treating the exception is a voluntary activity based on familiarity with the API). If the exception is not treated the thread will die.
  • We made out API more complicated – now the client code has to make two calls to access our functionality:
    if (account.hasFunds(amount)
    {
        transferFunds(amount, destination);
    }
    
  • We now have an API where some methods depend on other methods being called in the right order to successfully complete their job. This increases complexity and later will increase maintenance costs.

Solution number three – This is an exception indeed but we can recover from it and even take advantage of it

In this case we say yes, this can happen in normal business conditions, but still it is an exception in relationship with what the API tries to do (transfer funds). We decide to throw a checked exception and force the caller to do something about it.

public class NotEnoughFundsException extends Exception
{
    ...
}

public void transferFunds(long amount, Account destination)
    throws NotEnoughFundsException
{
    if (! this.hasFunds(amount)
    {
        throw new NotEnoughFundsException();
    }
    ...
}

The client code will have to look something like this:

try
{
    account.transferFunds(amount, destination);
}
catch(NotEnoughFundsException e)
{
    ... // treat the situation and maybe take advantage of it by offering credit
}

Of course the direct caller can also throw the exception but at some moment somebody has to treat it. It just can’t be ignored without thinking about it.

This solution solves most of the problems about the API and makes callers life safer and even easier to some degree.

On the other hand it looks like we actually use exceptions for flow control in this case, treating a normal business situation. This is true to some degree and here we are in the gray area where rules are not very clear. But keep in mind that what matters is the concrete situation and the requirements. How the situation changes if the requirement from the bank is to detect failed attempts to transfer funds and to automatically offer credit?
In this solution we decided to look at the problem from the point of view of the layer at which the API does its job.

All these solutions solve the problem with more or less complication for the client code. In deciding what kind of solution to use is important to understand all (at least more than one) possibilities and to make an educated decision based on your requirements and the desire to make life safe and as simple as possible for the programmer writing the client code.

I am not gonna say which is my favorite solution even if you can probably guess. Think about it and decide. None of them is fundamentally wrong, the context has a great influence on the final decision.

This very simple example shows why designing the exception scenarios and handling exceptions is very difficult and why people use different solutions: : solutions are not absolute, they depend on the how you look at the problem.

This post is part of a series on exceptions:

  1. Thoughts on Java exceptions
  2. Some exceptions are more equal than others
  3. Less than perfect exceptions hierarchy
  4. Checked exceptions are priceless… For everything else there is the RuntimeException
  5. Design the failure case – Part 1
  6. Design the failure case – Part 2
  7. Exception design relativity
  8. Bad advice on exceptions from Joel

5 thoughts on “Exceptional Java – Exception design relativity”

  1. Approach number 1 is correct. Period. All other attempts to force fit the exception syntax are foolhardy.

    The worst legacy of java is the pathological use of exceptions baked into the language, which has encouraged rampant misunderstanding and abuse of this mechanism. Then Redmond got into the act with their me-too language and perpetuated the trend.

    Get over it folks: the real world is messy. Don’t be so obssessed with the so called ‘sunny path’. Deal with ALL contingencies, explictly and cleanly.

    Throwing exceptions is only legitimate in very rare circumstances. Basically never in application code. All code which may encounter an exception, especially pathological exceptions from other API’s should wrap them by catch and convert to suitable return code.

    Every other approach is fundamentally flawed.

  2. Approach 2 is not a solution because it contains a race condition. Another thread could lower the customer’s account balance between the times that hasFunds() and transferFunds() are called. The check-and-transfer-funds sequence should be made atomic to avoid the need for external locking.

  3. I would suggest a hybrid approach.

    Check for available funds first, then attempt to transfer. If this fails because of lack of funds, throw an exception. In the normal path no exception is thrown. In the hopefully rare situation where multiple threads lower the available funds then the exception will save us.

    So in code

    try
    {
    if( hasFunds(amount)){
    account.transferFunds(amount, destination);
    } else {
    //….do something, same as below
    }
    }
    catch(NotEnoughFundsException e)
    {
    ///….do something
    }

    Of course it depends on how expensive the call to ‘hasFunds’ is. If it involves db calls, soap calls etc, it might be decided that relying on the exception is a better strategy as it uses less overall computer resources.

    Returning true/false if the transfer went ahead or not is not helpful as it doesn’t tell us why it failed. Lack of funds, db issues, third party providers down,exceeded transfer limits, transfer to non existent account, account locked ? etc. Lack of funds is only one of many reasons why it failed, for technical as well as business reasons. Compressing this all down into a single yes/no leaves the programmer no useful information, and consequently nothing to pass back to the user. Cept maybe a ‘computer says no’.

    Cheers,
    Bert

  4. After reading my own post, maybe the exception should be renamed to ‘FundsTransferException’, which possibly subclasses ‘FundsException’ (depending naturally on the rest of the application)

    This exception would possibly have an error code or enum field with the reason for the failure (db error, account locked etc…), or a wrapped cause.

Comments are closed.