Else If Without Else

ID

go.else_if_without_else

Severity

high

Remediation Complexity

trivial

Remediation Risk

low

Remediation Effort

low

Resource

Code Smell

Language

Go

Tags

code-style, conditional, control-flow

Description

Reports an if / else if chain that has no final else and whose branches can all fall through.

At least one else if is required: a lone if with no else is an ordinary guard and is never reported. A chain whose every branch ends the flow — return, break, continue, goto, panic(…​) or os.Exit(…​) — is also not reported; see Configuration.

Rationale

func tally(level string, c *counters) {
	if level == "warn" {                // FLAW — no final else
		c.warn++
	} else if level == "fail" {
		c.fail++
	}
}

A chain of conditions with no catch-all leaves one case unwritten: the one where none of the conditions hold. Nothing in the source says whether doing nothing is the intended behaviour for that case or whether the branch was forgotten, so a reader cannot tell a deliberate no-op from an oversight, and neither can a reviewer.

The gap gets worse over time. When a value is added to whatever level enumerates, the new value falls through the whole chain silently — the counter is simply never incremented, with no error and no log line. A chain that ends in else fails loudly instead, at the one place that was written to catch exactly this.

Remediation

End the chain with an else that states the remaining case, even when the statement is that nothing should happen.

func tally(level string, c *counters) {
	if level == "warn" {
		c.warn++
	} else if level == "fail" {
		c.fail++
	} else {
		c.other++
	}
}

When the remaining case really is unreachable, say so where a reader and a test will both see it:

} else {
	panic(fmt.Sprintf("unhandled level %q", level))
}

If the chain is dispatching on a single value, a switch with a default says the same thing more directly — see go.missing_switch_default.

Configuration

This rule has no configurable properties. Two shapes are excluded by construction.

Chains whose every branch terminates. When the if body and every else if body end in return, break, continue, goto, panic(…​) or os.Exit(…​), control provably leaves the chain in each branch, and the statements after the chain are the else case:

if n > 0 {
	return "positive"
} else if n < 0 {
	return "negative"
}

return "zero"          // OK — this is the else, unindented

That is the idiomatic Go early return, and go.superfluous_else asks for exactly it: writing the else back in would earn a finding from that rule. Reporting it here would put the two rules in direct contradiction on the same code, so both decide what "terminates" means the same way.

A lone if. One guard is not an enumeration, so if x { …​ } and if x { …​ } else { …​ } are outside the rule. What is left is the shape the rule is about: a ladder of conditions that only assigns or calls, and silently does nothing when no condition matches.

© 2026 Xygeni. All rights reserved