Exceptions as Flow Control in Java

Posted on Tue 27 December 2016 in Performance

Quite often I see Java code that relies on exceptions being thrown to indicate different states being returned for a method. As an example, a lot of API calls have requirements on what can be sent as input. Every time a requirement isn’t met, an exception is thrown and then caught by the request router. This is then properly formatted, and displayed back to the application that sent the invalid request.

This practice isn’t necessarily bad, however in cases where millions of a request are being sent with a moderate to high chance of error this can make a measurable impact to performance.

To test how much of an impact this has, I developed a simple JMH benchmark.

Benchmark                                     Mode  Cnt     Score    Error  Units
ReturnVsException.testException              thrpt  100    42.724 ±  2.056  ops/s
ReturnVsException.testFastException          thrpt  100  2014.997 ± 15.174  ops/s
ReturnVsException.testReturn                 thrpt  100  5492.222 ± 60.231  ops/s

The following code was used to perform this benchmark,

private int index;

@Setup(Level.Iteration)
public void setup() {
    index = 0;
}

@Benchmark
public void testReturn() {
    performReturn();
}

@Benchmark
public void testException() {
    try {
        performException();
    } catch (Exception e) {
    }
}

@Benchmark
public void testFastException() {
    try {
        performFastException();
    } catch (Exception e) {
    }
}

private boolean performReturn() {
    index ++;

    return index % 5 != 0;
}

private void performException() throws Exception {
    index ++;

    if (index % 5 == 0) {
        throw new IllegalArgumentException();
    }
}

private void performFastException() throws Exception {
    index ++;

    if (index % 5 == 0) {
        throw new FastIllegalArgumentException();
    }
}

public static class FastIllegalArgumentException extends IllegalArgumentException {

    @Override
    public synchronized Throwable fillInStackTrace() {
        return null;
    }
}

Now, this is a basic test that just returns a Boolean. If you were to replace exceptions with returns, you’d generally create an enumeration or similar for each API call to indicate different responses.

The following is a modified version of the test that returns an enumeration rather than a Boolean value, and the results are similar.

Benchmark                                     Mode  Cnt     Score    Error  Units
ReturnVsExceptionAdvanced.testException      thrpt  100    18.128 ±  0.269  ops/s
ReturnVsExceptionAdvanced.testFastException  thrpt  100   915.171 ±  9.073  ops/s
ReturnVsExceptionAdvanced.testReturn         thrpt  100  5491.713 ± 66.110  ops/s

The following code was used to perform this benchmark,

private int index;

@Setup(Level.Iteration)
public void setup() {
    index = 0;
}

@Benchmark
public void testReturn() {
    performReturn();
}

@Benchmark
public void testException() {
    try {
        performException();
    } catch (Exception e) {
    }
}

@Benchmark
public void testFastException() {
    try {
        performFastException();
    } catch (Exception e) {
    }
}

private APIResponse performReturn() {
    index ++;

    if (index % 5 == 0) {
        return APIResponse.TEST_1;
    } else if (index % 3 == 0) {
        return APIResponse.TEST_2;
    } else {
        return APIResponse.SUCCESS;
    }
}

private void performException() throws Exception {
    index ++;

    if (index % 5 == 0) {
        throw new IllegalArgumentException("Test 1");
    } else if (index % 3 == 0) {
        throw new IllegalArgumentException("Test 2");
    }
}

private void performFastException() throws Exception {
    index ++;

    if (index % 5 == 0) {
        throw new FastIllegalArgumentException("Test 1");
    } else if (index % 3 == 0) {
        throw new FastIllegalArgumentException("Test 2");
    }
}

public static class FastIllegalArgumentException extends IllegalArgumentException {

    public FastIllegalArgumentException(String s) {
        super(s);
    }

    @Override
    public synchronized Throwable fillInStackTrace() {
        return null;
    }
}

private enum APIResponse {
    TEST_1,
    TEST_2,
    SUCCESS
}

Both scores are slightly lower than the previous test, however this can be partially attributed to the extra modulo operation. In the case of the exception test, it is being thrown more than in the prior test. This highlights an important detail and caveat of using exceptions, the performance varies greatly depending on whether it threw an exception or not.

Overall there’s a difference of over 300x between the exception and return flow control types. Both methods allow the same level of control, yet one performs a lot better.

Both benchmarks also test what is often cited as a solution to this issue; creating a custom exception class to remove the stacktrace generation. This is shown by the 'fastException' test. Whilst this drastically speeds up exceptions, it doesn't perform on the same level as returns.

If you’ve currently got a massive application using exceptions in places like this, I’m not suggesting replacing it all with return statements. However, if you do end up having performance issues this is be a good place to start.

"exceptions are, as their name implies, to be used only for exceptional conditions; they should never be used for ordinary control flow"

(Quote from Effective Java (2nd Edition) (The Java Series) (2008) by Joshua Bloch)

Note: This article has been updated with a revised benchmark that also tests against custom exceptions, and less chance of error.