This content originally appeared on DEV Community and was authored by Alexandre Aquiles
Recently, during a code review, I came across something similar to the Java code below:
String token = //...
try {
int n = Integer.parseInt(token);
// do int stuff with n...
} catch (NumberFormatException e) {
// do string stuff with token...
}
This code attempts to parse token
as an integer. If it's not a number, it falls back to handling it as a string. Here, an exception is used as a conditional: to decide how to procede.
This has long been considered an anti-pattern, as discussed in Don't Use Exceptions for Flow Control from the legendary Portland Pattern Repository and in Joshua Bloch's superb Effective Java:
Item 69: Use exceptions only for exceptional conditions
Exceptions are, as their name implies, to be used only for exceptional conditions; they should never be used for ordinary control flow.
Someone on the PR commented that exceptions for control flow are not only less readable: they are slow.
Indeed! In Effective Java, Joshua shows how using exceptions for looping is twice as slow as a regular loop.
But how slow, exactly? I had to measure it.
Micro-benchmarking with JMH
Disclaimer: The nanoseconds saved here would be irrelevant if your bottleneck is a DB query that takes hundreds of miliseconds.
I've used JMH (Java Microbenchmark Harness) for precise benchmarking.
I compared the following alternative ways of checking if a given token is a number:
- Using
Integer.parseInt
andNumberFormatException
: is this really slow? - Using an uncompiled regex: a simple
"-?\\d+"
regex to determine iftoken
matches. - Using a compiled regex: leverages a precompiled Pattern object for regex matching.
- Using a custom check method: iterates over
token
checking withCharacter.isDigit
if each character is a valid integer.
In my project's build.gradle
, I added the JMH plugin:
plugins {
id 'java'
id "me.champeau.jmh" version "0.7.3"
}
In JMH, we can measure performance in different ways, such as average time per operation or operations per second. I preferred to measure average time in nanoseconds, as it gives more intuitive insight into performance per call.
Here’s the overall structure of the benchmark class:
import org.openjdk.jmh.annotations.*;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Thread)
public class TokenBenchmark {
@Param({"42", "+", "-13", "hello", "-"})
public String token;
private static final Pattern NUM_REGEX = Pattern.compile("-?\\d+");
}
Let’s break that down:
-
@BenchmarkMode(Mode.AverageTime)
and@OutputTimeUnit(TimeUnit.NANOSECONDS)
configure the benchmark to measure the average time per operation, in nanoseconds -
@State(Scope.Thread)
ensures each thread runs with its own state instance, avoiding interference between benchmark runs. -
@Param
defines the set of input values used for each benchmark. I included both numeric ("42", "-13") and non-numeric ("+", "hello", "-") tokens. -
NUM_REGEX
constant defines a precompiled regex pattern to be reused by the benchmark.
This setup lets JMH inject different inputs and accurately compare each parsing strategy in isolation.
Then I defined a method for each approach and annotated them with @Benchmark
:
@Benchmark
public boolean usingParseInt() {
try {
Integer.parseInt(token);
return true;
} catch (NumberFormatException e) {
return false;
}
}
@Benchmark
public boolean usingRegex() {
return token.matches("-?\\d+");
}
@Benchmark
public boolean usingRegexCompiled() {
return NUM_REGEX.matcher(token).matches();
}
@Benchmark
public boolean usingCharCheck() {
if (token == null || token.isEmpty()) return false;
int start = token.charAt(0) == '-' ? 1 : 0;
if (start == 1 && token.length() == 1) return false;
for (int i = start; i < token.length(); i++) {
if (!Character.isDigit(token.charAt(i))) return false;
}
return true;
}
Finally, I ran the benchmark using the Gradle JMH plugin:
./gradlew jmh
Results
It took a while to run, but the results were the following, organized by method and input data:
Benchmark (token) Mode Cnt Score Error Units
TokenBenchmark.usingCharCheck 42 avgt 25 2.741 ± 0.005 ns/op
TokenBenchmark.usingCharCheck + avgt 25 1.246 ± 0.019 ns/op
TokenBenchmark.usingCharCheck -13 avgt 25 3.159 ± 0.023 ns/op
TokenBenchmark.usingCharCheck hello avgt 25 1.444 ± 0.005 ns/op
TokenBenchmark.usingCharCheck - avgt 25 1.189 ± 0.017 ns/op
TokenBenchmark.usingParseInt 42 avgt 25 3.737 ± 0.016 ns/op
TokenBenchmark.usingParseInt + avgt 25 993.799 ± 6.810 ns/op
TokenBenchmark.usingParseInt -13 avgt 25 4.158 ± 0.008 ns/op
TokenBenchmark.usingParseInt hello avgt 25 992.356 ± 5.334 ns/op
TokenBenchmark.usingParseInt - avgt 25 992.391 ± 4.740 ns/op
TokenBenchmark.usingRegex 42 avgt 25 85.985 ± 0.721 ns/op
TokenBenchmark.usingRegex + avgt 25 85.185 ± 0.918 ns/op
TokenBenchmark.usingRegex -13 avgt 25 91.964 ± 3.020 ns/op
TokenBenchmark.usingRegex hello avgt 25 68.243 ± 4.317 ns/op
TokenBenchmark.usingRegex - avgt 25 66.229 ± 1.719 ns/op
TokenBenchmark.usingRegexCompiled 42 avgt 25 35.011 ± 0.328 ns/op
TokenBenchmark.usingRegexCompiled + avgt 25 44.729 ± 12.679 ns/op
TokenBenchmark.usingRegexCompiled -13 avgt 25 39.952 ± 0.442 ns/op
TokenBenchmark.usingRegexCompiled hello avgt 25 59.894 ± 10.058 ns/op
TokenBenchmark.usingRegexCompiled - avgt 25 46.192 ± 14.258 ns/op
A quick summary of the results:
- Slowest for invalid input: usingParseInt (~993 ns)
- Intermediate: usingRegex or usingRegexCompiled (~35–90 ns)
- Fastest: usingCharCheck (e.g., 1–3 ns)
Why so slow?
Exceptions are that slow for control flow because throwing and catching an exception is a costly operation in most runtimes, including the JVM.
When an exception is thrown, the JVM captures the current call stack to generate a stack trace, which involves significant overhead compared to a simple conditional check.
This makes exceptions suitable for truly exceptional, infrequent events—not for regular, expected logic paths like parsing input.
So, using exceptions for control flow isn’t just bad style. It’s orders of magnitude slower.
Code here: https://github.com/alexandreaquiles/token-benchmark
This content originally appeared on DEV Community and was authored by Alexandre Aquiles

Alexandre Aquiles | Sciencx (2025-06-07T13:32:55+00:00) Exceptions for Control Flow are Slow. Retrieved from https://www.scien.cx/2025/06/07/exceptions-for-control-flow-are-slow/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.