When I joined Infobip, I was introduced to the team’s coding rules. One of them emphasized using ‘else’ blocks sparingly.

The rule seemed interesting, but understandable at the same time. Conditionals are essential, but overuse can clutter the code.

So, I aimed to keep my conditional blocks concise. I opened my first pull request with a code similar to this one:

public void performTextAnalysis(String text) {
    AnalysisResult result = analyzer.analyze(text);
     
    if (result.isSuccessful()) {
        successfulAnalysisCounter.increment();
    } else {    
        failedAnalysisCounter.increment();
    }
     
    results.save(result);
}

After completing some analysis, we increased the relevant counter and saved the results.

But then, a colleague suggested I rewrite the code without using the ‘else’ block. I was confused, so I asked for clarification. Their response? We don’t write ‘else’ blocks!?

A voice of reason

This response got under my skin. I believed the code I had written was crystal clear, far more so than the alternative proposed:

public void performTextAnalysis(String text) {
    AnalysisResult result = analyzer.analyze(text);
     
    recordAnalysisMetrics(result);
     
    results.save(result);
}
 
private void recordAnalysisMetrics(AnalysisResult result) {
    if (result.isSuccessful()) {
        successfulAnalysisCounter.increment();
        return;
    }
 
    failedAnalysisCounter.increment();
}

I readied my sword and shield, prepared to defend my pristine code at any cost.

Just as I was about to dive into battle, my senior peer stepped in with a voice of reason. He acknowledged that my code was indeed readable, perhaps even slightly more than the alternative. Yet, he advocated for the no-else option, not because it made the code prettier, but because it simplified our coding standards

This argument sat well with me because I could imagine a lot of pull request arguments (coding preferences) resolved easily. So I agreed with him and decided to concede this time.

Despite the resulting code looking acceptable, I remained convinced that there would be instances where avoiding the ‘else’ statement would lead to a convoluted mess. I would triumphantly point to my peers and say ‘See! This is why we should not avoid else statements’! 

So, let’s find a code example where using ‘else’ really matters!

No-else code examples

Let’s start with some simple cases where ‘else’ blocks are used.

Input validation

One example involves checking input validity. For instance, suppose the performTextAnalysis function expects non-trivial text as input.

public void performTextAnalysis(String text) {
    if (!isNullOrEmpty(text)) {
        AnalysisResult result = analyzer.analyze(text);
         
        recordAnalysisMetrics(result);
     
       results.save(result);
    } else {
        throw new IllegalStateException("Text is empty");
    }
}

The current code, although straightforward, is readable. However, as requirements evolve and more code is added to the core of the method, the ‘else’ block may become separated from its corresponding ‘if’ condition

This could lead to the need for scrolling back and forth between the ‘else’ block and the ‘if’ condition to understand the code. By avoiding the use of ‘else’, you have no choice but to handle the exceptional branch first before delving into the core logic of the method.

public void performTextAnalysis(String text) {
    if (isNullOrEmpty(text)) {
        throw new IllegalStateException("Text is empty");
    }
 
    AnalysisResult result = analyzer.analyze(text);
     
    recordAnalysisMetrics(result);
     
    results.save(result);
}

It is convenient that throwing an exception automatically closes a conditional branch. What if we wanted to simply skip the analysis in case the text is empty? In that case, we would simply return instead of throwing an exception.

public void performTextAnalysis(String text) {
    if (isNullOrEmpty(text)) {
        log.info("Text is empty. Skipping analysis...");
        return;
    }
 
    AnalysisResult result = analyzer.analyze(text);
 
    recordAnalysisMetrics(result);
 
    results.save(result);
}

One could argue that the previous example simply demonstrates how input validation should be done at the start of a method, which is a valid point. Enforcing a rule like ‘validate input at the beginning of the function’ could achieve the same result. A more general principle might be to handle simpler branches before delving into complex ones. However, none of these rules can match the straightforwardness of the ‘no else’ rule.

Conditional business logic

In this scenario, there isn’t a ‘less important’ branch that we can easily close early. All conditional branches are equally important. For example, there might be multiple analyses we can perform on a given text, depending on the alphabet used.

public void performTextAnalysis(String text) {
    if (isNullOrEmpty(text)) {
        throw new IllegalStateException("Text is empty");
    }
 
    AnalysisResult result;
    if (isLatin(text)) {
        result = analyzer.analyzeLatin(text);
    } else if (isCyrillic(text)) {
        result = analyzer.analyzeCyrillic(text); 
    } else if (isChinese(text)) {
        result = analyzer.analyzeChinese(text); 
    } else if (isArabic(text)) {
        result = analyzer.analyzeArabic(text); 
    } else {
        throw new IllegalArgumentException("Unrecognized alphabet");
    }
     
    recordAnalysisMetrics(result);
     
    results.save(result);
}

In this case, we cannot exit the method in the if-block anymore since we would be bypassing all the subsequent logic. When the ‘no else’ rule is enforced you do not have a choice, but to extract this block into a new method.

public void performTextAnalysis(String text) {
    if (isNullOrEmpty(text)) {
        throw new IllegalStateException("Text is empty");
    }
 
    AnalysisResult result = doAnalyze(text);
     
    recordAnalysisMetrics(result);
     
    results.save(result);
}
 
private void doAnalyze(String text) {
    if (isLatin(text)) {
        return analyzer.analyzeLatin(text);
    }
    if (isCyrillic(text)) {
        return analyzer.analyzeCyrillic(text); 
    }
    if (isChinese(text)) {
        return analyzer.analyzeChinese(text); 
    }
    if (isArabic(text)) {
        return analyzer.analyzeArabic(text); 
    }
 
    throw new IllegalArgumentException("Unrecognized alphabet");
}

One could argue that this code could have been cleaner by:

  • Using the latest Java pattern-matching syntax
  • Separating language recognition from text analysis strategy

However, the key point of this example was to demonstrate that the code became cleaner simply by adhering to a very basic rule. Why is this approach cleaner than having it inlined? Because the list of alphabets isn’t exhaustive, adding more cases could overwhelm the method, resulting in decreased readability.

No else → No choice

The more I worked with the ‘no else’ rule, the more I appreciated its benefits. It inherently guides you towards writing simpler methods. Sure, you can still create complex methods using only ‘if’ blocks, but it requires deliberate effort and thought. 

As developers, we cherish having choices and tend to resist any form of constraint. However, constraints can be beneficial. Consider this:

Why do modern languages no longer support GO TO statements?

 Why do developers dread friend functions in C++?

Constraints serve to prevent developers from writing unreadable code while still enabling them to write good code.

There are only two techniques required to write code without ‘else’ blocks.

1.    Closing simple conditional branches early

2.    Extract more complex conditional blocks into new methods

With these strategies, any code can be written without the need for ‘else’. However, there’s a scenario where avoiding ‘else’ can noticeably impair code readability

Extracting methods incur a cost – the flow is interrupted, and the reader must navigate back and forth to comprehend the flow. This is why we typically keep higher-level concepts together while extracting lower-level ones into methods. In both examples mentioned earlier, we extracted lower-level concepts into new methods.

But what if having a conditional is an integral part of the core business logic? For instance, when an analysis fails, we need to persist the text into a database for troubleshooting and re-processing purposes. When an analysis is successful, we need to send an email that the analysis has been completed.

public void performTextAnalysis(String text) {
    if (isNullOrEmpty(text)) {
        throw new IllegalStateException("Text is empty");
    }
 
    AnalysisResult result = doAnalyze(String text);
    recordAnalysisMetrics(result);
     
    if (result.isSuccessful()) {
        emailService.notifyAnalysisComplete();
    } else {     
        notAnalyzableTexts.save(text);
    }
 
    results.save(result);
}

In scenarios where a conditional is fundamental to the core business logic, finding an elegant solution can be challenging. We could:

  1. Extract the code into a vaguely named method like ‘handleResult’
  2. Extract the code into an over-descriptive method like ‘recordAnalysisMetricsAndRetryIfAnalysisFailed’

Both options disrupt the flow of reading and can make the code less intuitive. While there are workarounds, they aren’t as straightforward as the two listed. This highlights that the ‘no else’ rule isn’t flawless. Without actively considering the code we write, we can still end up with clunky solutions.

The alternative

Let’s examine the alternative approach and consider the kind of clunky code that could result without adhering to the ‘no else’ rule.


public void performTextAnalysis(String text) {
    if (!isNullOrEmpty(text)) {
           
        AnalysisResult result;
        if (isLatin(text)) {
            result = analyzer.analyzeLatin(text);
        } else if (isCyrillic(text)) {
            result = analyzer.analyzeCyrillic(text); 
        } else if (isChinese(text)) {
            result = analyzer.analyzeChinese(text); 
        } else if (isArabic(text)) {
            result = analyzer.analyzeArabic(text); 
        } else {
            throw new IllegalArgumentException("Unrecognized alphabet");
        }
     
        if (result.isSuccessful()) {
            successfulAnalysisCounter.increment();
            emailService.notifyAnalysisComplete();
        } else {    
            failedAnalysisCounter.increment();
            notAnalyzableTexts.save(text);
        }
     
       results.save(result);
    } else {
        throw new IllegalStateException("Text is empty");
    }
}

While the code snippet provided may not seem overly cumbersome with its 27 lines, anyone who has dealt with legacy code knows the perils of encountering monstrous methods with overwhelming cyclomatic complexity. Each developer likely began with good intentions, but as the code evolved, so did its size and complexity.

You might be confident in your coding practices, thinking, ‘I would never write methods like these.’ And you might be correct; perhaps you wouldn’t. But can you make the same guarantee for your coworkers?

I’m fortunate to work in a team where everyone shares my passion for clean coding, and I’m confident that we’ve outgrown the need for the ‘no else’ rule. However, not every team or workplace enjoys such a luxury. Writing code without ‘else’ provides a foolproof way to ensure clarity and maintainability, and you can’t go wrong with it.