Avoid Using Exceptions for Control Flow — Design Better Error Handling?
Why treating exceptions like logic gates is an anti-pattern — and what you should do instead
Hello guys, as a senior software engineer, part of my job is to review code and mentor junior developers. One common anti-pattern I often come across is using exceptions to control the flow of a program.
While exceptions are powerful tools for error handling, misusing them can lead to brittle, unreadable, and inefficient code and shoudn’’t be used for control flow at all.
In my last a couple of articles I have shared coding and refactoring tip about why using Enum is better than boolean in method parameters, stop using if-else chains and why refactor method with more than 3 parameters and feedback was awesome.
You guys loved it. So, I am sharing another common coding insight which I noticed and learned by doing code reviews.
Let me explain what I mean, and how you can do better.
🚫 Problem: Misusing Exceptions for Control Flow
Here’s a typical example I’ve seen in production code:
At first glance, this may seem fine. But this approach abuses exceptions as part of normal program logic.
Why is this bad?
Exceptions are expensive: Throwing and catching exceptions is computationally expensive compared to regular control structures.
In fact, just having try-catch block means JVM cannot optimize your code for many rules it will otherwise do, causing some performance hit.Hurts readability: Readers now need to trace multiple try-catch blocks to understand the flow.
Breaks intention: Exceptions should be used for unexpected conditions, not expected validations.
✅ Solution: Use Explicit Validation Instead
A cleaner and more readable approach is to validate first and use clear return types or domain-specific error handling:
Even better: Return a result wrapper that captures success/failure and reason:
Now your calling code can handle this safely:
🔍 Insight and Analysis
Using exceptions for normal flow control is like using a fire alarm every time you leave a room — it’s noisy, inefficient, and not what it was designed for.
In most languages (especially Java, Python, C#, etc.), exceptions are for exceptional circumstances — things that aren’t part of your expected logic, like network failures, IO errors, or corrupted data from external sources.
Instead, use types and control structures that model your domain:
Use
Optional
,Either
, orResult
types (available in Java, Kotlin, Rust, TypeScript, etc.).Validate inputs before performing operations.
Design clear APIs that separate validations from failures.
This not only improves performance and maintainability but it also helps your team better understand the logic at a glance.
How CodeRabbit Can Help You Avoid Exception Misuse?
Tools like CodeRabbit are especially useful in enforcing best practices during code reviews.
As an AI-powered code review assistant, CodeRabbit can automatically detect patterns where exceptions are used inappropriately for control flow—such as using try-catch
blocks instead of proper conditionals or returning error codes.
It can highlight these issues early during pull requests and suggest cleaner, more maintainable alternatives.
By integrating with your GitHub workflow, CodeRabbit helps enforce team-wide error handling conventions, encourages the use of Result or Either types (in languages like Java, Kotlin, or TypeScript), and ensures junior developers learn to write production-ready, resilient code.
This kind of real-time, contextual feedback can be a game-changer in improving software quality and reducing bugs in production.
Here is how it works
Conclusion
As developers, we often reach for exceptions because they’re easy and available. But part of growing as an engineer is knowing when not to use them.
In code reviews, I encourage juniors to ask themselves:
Is this really an exceptional case, or is it just something I should check for upfront?
When we stop using exceptions for control flow, our code becomes clearer, safer, and much easier to maintain.
Other Coding and Tech Articles you may like
I use an object as you did at your last example. This object contains a boolean, a payload (data) and a String (message).
When the boolean is a false, either I print the message by using a System.err or it is sent to a output system (email, local logging, slack channel or whatnot).
For that reason is that I like the Golang approach. The returns can give yout the error + the result