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)
About the Author
Hi, I'm Maddy Miller, a Senior Software Engineer at Clipchamp at Microsoft. In my spare time I love writing articles, and I also develop the Minecraft mods WorldEdit, WorldGuard, and CraftBook. My opinions are my own and do not represent those of my employer in any capacity. Find out more.