It is widely accepted mantra that exceptions should not be used for control flow. The main arguments against that practice have to do with code structuring, pointing out that exceptions are not very different from “go-to” statements, which are considered harmful, and that even in languages that support exceptions there are often better ways to handle errors. Exceptions can make the code harder to read, harder to maintain/evolve, harder to test and harder to debug.

When should exceptions be used then? The general rule of thumb is that exceptions should be used for truly exceptional situations, that is, conditions that are not part of the normal flow of the program or that are difficult to predict, like StackOverflowError or OutOfMemoryError. On the opposite side of the spectrum, exceptions must not be used for conditions that are part of the normal flow of the program, like a user entering an invalid input.

One of the arguments against using exceptions for control flow is their poor performance in languages like Java. But how bad is it really? Let’s take a look at the following class:

public class DeliveryService {
    public String deliver_withExceptions(String postalCodeOrTown) {
        try {
            return deliverToPostalCode(Integer.parseInt(postalCodeOrTown));
        } catch (NumberFormatException e) {
            return deliverToTown(postalCodeOrTown);
        }
    }

    private String deliverToPostalCode(int postalCode) {
        return "Delivering to postal code " + postalCode;
    }

    private String deliverToTown(String town) {
        return "Delivering to town " + town;
    }
}

For users’ convenience, this DeliveryService class allows them to provide either a postal code or a town name as input. It will try to parse the input as an integer (postal code) and fallback to delivering to the town name if it fails. It does that by wrapping the call to Integer.parseInt in a try/catch block, and recovering from the NumberFormatException.

Let’s now write a very un-scientific performance test to get a sense of how this code performs:

 public static void main(String[] args) {
     var deliveryService = new DeliveryService();

     var start = System.nanoTime();

     for (long i = 0; i < 10_000_000; i++) {
         deliveryService.deliver_withExceptions("Springfield");
     }

     var finish = System.nanoTime();

     System.out.println("Time elapsed: " + (finish - start)/1_000_000 + " ms");
 }

This test will call the method 10 million times, and measure the time it takes to do so. On my laptop, takes around 4.4 seconds. That does not sound too bad, does it?

We can refactor the code to avoid exceptions. One option is to use a very simple regular expression to check if the input is a number before trying to parse it. Another option is to use a library like Apache Commons Lang, which provides a NumberUtils.isNumeric method. The code looks like this:

    public String deliver_withRegex(String postalCodeOrTown) {
        if (postalCodeOrTown.matches("\\d+")) {
            return deliverToPostalCode(Integer.parseInt(postalCodeOrTown));
        } else {
            return deliverToTown(postalCodeOrTown);
        }
    }

    public String deliver_withIsNumeric(String postalCodeOrTown) {
        if (isNumeric(postalCodeOrTown)) {
            return deliverToPostalCode(Integer.parseInt(postalCodeOrTown));
        } else {
            return deliverToTown(postalCodeOrTown);
        }
    }

Let’s run the same test with these two new methods, again with 10 million iterations. The results are:

Method Time elapsed (ms)
withExceptions 4,400
withRegex 620
withIsNumeric 30

As mentioned before, this is a very un-scientific test. Nevertheless, the performance difference is staggering. The version with regular expressions is 7 times faster, despite regular expressions being known for being slow. The version with NumberUtils.isNumeric is 146 times faster.

There are a number of reasons why the version with exceptions is so slow. One of them is that exceptions are expensive to create because by default they include a stacktrace, which basically requires an inspection of the contents of the stack. Another reason is that exceptions disrupt the “predictable” flow of the program, which impedes some of the optimisations that the JVM and the microprocessors can do.

Some clever readers may have noticed that the “improved” versions without exceptions are not perfect. For example, both the implementation with a regular expression and the NumberUtils.isNumeric one will fail if the input is "12345678901" because although it matches the regular expression and is numeric, that number is too large to be parsed as an integer, which is what we need in order to call deliverToPostalCode. We may actually need to handle that case with a try/catch, after all. But that case falls into the category of exceptional situations, where an exception may be the right tool for the job. It can be argued that "12345678901" is neither a valid postal code nor a valid town name, and therefore it is not a valid input. Therefore in that case we would not be using exceptions for control flow as part of processing valid inputs, but for actual error handling.