Exceptions for Control Flow are Slow

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 and NumberFormatException: is this really slow?
  • Using an uncompiled regex: a simple "-?\\d+" regex to determine if token matches.
  • Using a compiled regex: leverages a precompiled Pattern object for regex matching.
  • Using a custom check method: iterates over token checking with Character.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)

The results show that the precompiled regex is a good choice if you want something simple and fast enough for most cases.

However, if this check runs frequently and performance is critical, the usingCharCheck method is the fastest. It avoids the overhead of regex matching and is better suited for performance-sensitive code paths.

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

SOLID Principles Aren’t Principles

Have you ever found yourself pondering endlessly about whether a piece of code follow this or that SOLID principle? It’s as if following those principles is more important than the problem itself.

A hypothesis is that it has to do with the word “principle”. If SOLID are truly principles, we must always follow them. But should we?

To address this question, let’s first revisit SOLID, then study some definitions of “principle”, and finally evaluate if SOLID aligns with any of those definitions.

Revisiting SOLID

S.O.L.I.D. is a set of design so-called principles coined as an acronym by Robert “Uncle Bob” Martin, with each letter representing a key aspect of software design:

  • S for the Single Responsibility Principle: a particular view of cohesion that organizes code around potential changes.
  • O for the Open-Closed Principle: a way of promoting extensibility through abstractions.
  • L for the Liskov Substitution Principle: a strong view of sub-typing.
  • I for the Interface Segregation Principle: a technique for minimizing the “surface area” of non-cohesive code.
  • D for the Dependency Inversion Principle: an approach that decouples policies and business rules from implementation details.

These ideas are quite useful in Object-Oriented code and likely to be applied to other paradigms. But are they truly principles?

Fun fact: the concepts above were first published in book format in Agile Principles, Patterns, and Practices, released in 2002. But the acronym SOLID itself never appeared in this original edition nor in the 2006 C# edition. The order in the table of contents for both editions would form S.O.L.D.I.!

Defining Principles

What are principles? Let’s consult the Merriam-Webster dictionary:

  1. a comprehensive and fundamental law, doctrine, or assumption
  2. rule or code of conduct
  3. the laws or facts of nature underlying the working of an artificial device
  4. a primary source, origin
  5. an underlying faculty or endowment
  6. an ingredient (such as a chemical) that exhibits or imparts a characteristic quality
  7. a divine principle, GOD (when capitalized)

Are SOLID laws or rules? It doesn’t seem to be the case. Maybe they’re about natural facts or origin? Unlikely.

Examining Interface Segregation

Now that we’ve looked at what principles really mean, let’s examine the Interface Segregation Principle (ISP) a bit closer. It states that “Clients should not be forced to depend on methods that they do not use.”

When we have a non-cohesive piece of code, its clients end up having access to unneeded behavior.

Think of a class with numerous unrelated methods like the following:

 public class Employee { public BigDecimal calculatePayment() { //... } public BigDecimal calculateTaxes() { //... } public BigDecimal calculateOvertime(List<Hour> hours) { //... } public void saveToDB() { //... } public static Employee retrieveById(Long id) { //... } public String toXML() { //... } public static Employee fromXML(String xml) { //... } } 

The Employee class above deals with:

  • Payment, taxes and overtime.
  • Saving and retrieving objects from a database.
  • Converting to/from XML.

Most of the time, code using this class only needs some of its methods. For example:

  • Finance-related code might use calculatePaymentcalculateTaxes, and calculateOvertime.
  • Persistence tasks would use saveToDB and retrieveById
  • Integration with other systems might call toXML and fromXML.

This situation goes against the ISP. Clients of the problematic class could mix up different usages, spreading lack of cohesion throughout the codebase.

A solution would be to create separate interfaces for each group of clients, eliminating dependency on unused behavior. Hence, we would segregate behavior to narrowly focused interfaces such as:

 public interface EmployeeFinance { BigDecimal calculatePayment(); BigDecimal calculateTaxes(); BigDecimal calculateOvertime(List<Hour> hours); } public interface EmployeePersistence { void saveToDB(); static Employee retrieveById(Long id); } public interface EmployeeXmlSerialization { String toXML(); static Employee fromXML(String xml); } 

Our Employee class would implement the finer-grained interfaces above:

 public class Employee implements EmployeeFinance, EmployeePersistence, EmployeeXmlSerialization { // rest of the code... } 

Now, clients of Employee could use it behind a specific mask like EmployeeFinance for finance-related code and so on, without even knowing about other behaviors.

Identifying the root cause

The underlying problem here lies in lack of cohesion: a class is doing too much. And this means it’s a violation of the Single Responsibility Principle (SRP), as Employee has at least three reasons to be changed, related to finance, persistence and system integration. We should refactor this class to be more cohesive, having less reasons to change.

Actually, this is almost the exact example arguing for SRP in Robert Martin’s book UML for Java Programmers.

If breaking apart a troublesome class isn’t an option, we can apply Interface Segregation.

Therefore, Interface Segregation is not much a principle as it’s a technique or a pattern: a solution in the specific context of not being able to refactor non-cohesive code.

Exploring the Author’s Perspective

So, if the ideas in SOLID aren’t truly principles, what do they represent?

Let’s turn to their creator, Robert Martin, who, in the 2002 book Agile PPP, described them as:

“These principles are the hard-won product of decades of experience in software engineering.
They are not the product of a single mind but represent the integration of the thoughts and writings of a large number of software developers and researchers.”

Hence, SOLID emerges more as a collection of pragmatic advice from seasoned software engineers rather than immutable laws or rules.

In his 2009 blog post Getting a SOLID start, Martin clarified his use of the term “principle”:

“The SOLID principles are not rules.
They are not laws.
They are not perfect truths.
They are statements on the order of ‘An apple a day keeps the doctor away.’
This is a good principle, it is good advice, but it’s not a pure truth, nor is it a rule.”

Martin further suggests that SOLID principles serve as mental frameworks for addressing common software development challenges:

“The principles are mental cubby-holes.
They give a name to a concept so that you can talk and reason about that concept. […]
These principles are heuristics.
They are common-sense solutions to common problems. They are common-sense disciplines that can help you stay out of trouble.
But like any heuristic, they are empirical in nature. They have been observed to work in many cases; but there is no proof that they always work, nor any proof that they should always be followed.”

Characterizing SOLID as names for common solutions to recurrent problems resembles the definition of design patterns. It offers guidance within a specific context.

In his 2003 book UML for Java Programmers, Martin argues that we shouldn’t always apply SOLID and shows the effects of doing so:

“It is not wise to try to make all systems conform to all principles all the time, every time.
You’ll spend an eternity trying to imagine all the different environments to apply to the [Open/Closed Principle], or all the different sources of change to apply to the [Single Responsibility Principle].
You’ll cook up dozens or hundreds of little interfaces for the [Interface Segregation Principle], and create lots of worthless abstractions for the [Dependency Inversion Principle].”

So, are SOLID principles really principles? No. They’re more like guidelines.

By viewing SOLID as guidelines coming from pragmatic advice, we can avoid needless complexity in our code and make more effective design decisions considering the unique contexts of our projects.

Restrictive Abstractions

Recently, I was discussing with João Júnior, an experienced software engineer and a close friend, about how we are sometimes tempted to create abstractions that end up restricting us.

A Caching Example

Caching is a must-have for most applications because it reduces the time to retrieve frequently accessed data, thus improving performance. We usually implement caching with in-memory key-value data stores such as Redis or Memcached.

A simple abstraction for caching would enable us to perform operations such as associating key-value pairs, retrieving and deleting them, handling expiration and checking for existing keys.

We could materialize this abstraction in the following Java interface:

public interface Cache { boolean set(String key, String value); String get(String key); boolean delete(String key); boolean expire(String key, int seconds); boolean exists(String key); }

This interface is a simplified version of real caching abstractions from Java technologies such as the ones from Spring or JCache (JSR-107). Both are part of quite complex solutions, having more generic types and different capabilities. Also, annotations would be preferred to using Cache directly in most Java applications.

Sure, we could improve error handling and cache invalidation in the interface above. But our neat Cache abstraction could go very far! This abstraction would hide away the complexities of implementing clients for caching mechanisms like Redis or Memcached.

Eventually, if we decide to adopt any other caching mechanism with better scalability or performance, we could do it just by creating a new implementation for this interface, without changing (almost) any other code. The Open/Closed Principle in action!

When Abstraction Gets in Our Way

Some abstractions can present challenges in subtle ways. And even a well-designed abstraction might need to be adjusted if context changes.

What if we need to use Redis features like geospatial indexes, probabilistic data structures or even transactions? Our current simple abstraction would not fit those new use cases.

We might consider modifying our Cache interface to throw UnsupportedOperationException for implementations like Memcached, which lack those advanced features. However, this approach would not be a genuine solution but a consequence of weakening the notion of subtyping.

As we start depending more and more on Redis-specific capabilities, it would be difficult to generalize them back to other caching mechanisms.

In such a scenario, an abstraction that promoted extensibility and freed us to try new implementations would start to tie us up to old assumptions.

What would be our options to solve this? We could:

  • Reopen the closed abstraction, trying to find a new generalization.
  • Coexist the current abstraction for basic use cases with implementation-specific code for advanced functionalities.
  • Discard the current abstraction altogether.

The Spring framework, for instance, chose to have both generic and specific abstractions. In Spring applications, we can use caching abstractions for simpler caching needs. If we need specific capabilities, we could adopt more specialized modules such as Spring Data Redis.

ANSI SQL Can Be Restrictive

SQL is another example of a very sophisticated abstraction that can be restrictive—and leaky.

We can go very far using standard ANSI SQL. But, eventually, we end up having code specific to PostgreSQL or MySQL to optimize for performance.

Adhering to ANSI SQL could become so restrictive that it might prevent us from solving bottlenecks as data volume increases.

If our scenario really demands more than one database—for instance, software deployed on-premise within our client’s infrastructure—we would probably have to maintain separate optimizations for each one of them. This would lead to code duplication but, paraphrasing Sandi Metzprefer duplication over the restrictive abstraction.

So, Are Abstractions Useless?

Hold on! Abstractions are quite useful.

The absence of well-designed abstractions could result in code difficult to understand and to adapt to changing requirements.

And we can reap the benefits even from simple abstractions. We can go very far if we’re lucky enough to stick to basic usages.

But context changes. Trying to fit the new uses on the existing abstractions can be like fitting a square peg in a round hole.

No abstractions are definitive, closed for ever. We should keep revisiting them, evaluating if our scenario and assumptions are still the same.

If our current abstractions are too restrictive, we should rethink them.

Dev Multitask – React da Review Desbravando SOLID