I often see Java code that relies on thrown exceptions to indicate different output states of a method. As an example, a lot of API calls have requirements on what is valid input. Every time a condition fails, an exception is thrown and caught by the request router. This error is then adequately formatted and displayed back to the application that sent the invalid request.

This practice may not seem bad at a surface level; however, in cases where millions of requests occur with a moderate to high chance of error, this can make a measurable impact on performance.

The Benchmark

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

I used the following code 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 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

I used the following code 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
}

Discussion

Both scores are slightly lower than the previous test. However, this is partially due to the extra modulo operation. In the case of the exception test, more exceptions are thrown than in the previous test. This finding highlights a critical 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 much better.

Both benchmarks also test what is often cited as a solution to this issue, creating a custom exception class to remove the stack trace generation. The 'fastException' test shows this case. Whilst this drastically speeds up exceptions, it still 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 an excellent place to start.

Slow exception calls are often a symptom of a more significant problem of poor application design. Exceptions are intended for exceptional circumstances, not flow control. If thrown exceptions are occurring enough to cause noticeable performance degradation, they're likely not used for purely exceptional circumstances.

"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)