Missing Switch Default

ID

go.missing_switch_default

Severity

high

Remediation Complexity

trivial

Remediation Risk

low

Remediation Effort

low

Resource

Reliability

Language

Go

Tags

control-flow, reliability, switch

Description

Reports a switch that dispatches on a value and has no default clause.

Only a switch with a switch expression qualifies. An expression-less switch, a bool switch that handles both true and false, a switch with no clauses, a switch whose every case terminates with a statement after it, and a type switch are all excluded — see Configuration. select is a different construct and is never reported.

Rationale

func tally(level string, c *counters) {
	switch level {                      // FLAW — an unknown level is silently ignored
	case "warn":
		c.warn++
	case "fail":
		c.fail++
	}
}

Without a default, a value matching no case falls straight through and the switch does nothing. Go’s switch has no fallthrough by default and no exhaustiveness check, so this is entirely silent: no compiler warning, no panic, no log line. The caller sees a function that returned normally and did not do its job.

The failure mode that matters is the one that arrives later. When a value is added to whatever the switch dispatches on — a new status, a new message kind, a new enum constant — every switch over it that lacks a default starts quietly ignoring the new value, and the bug surfaces far from the code that caused it. A default is the one place written to catch that.

It also bites on the return path. A switch with no default inside a function that returns a value will fall out of the switch and return the zero value — an empty string, 0, nil — which is frequently indistinguishable from a legitimate result.

Remediation

Add a default that handles the remaining values, rejects them, or declares them impossible.

// Handle the rest.
switch level {
case "warn":
	c.warn++
case "fail":
	c.fail++
default:
	c.other++
}

// Or reject them, so the caller finds out.
switch level {
case "warn", "fail":
	return nil
default:
	return fmt.Errorf("unknown level %q", level)
}

// Or state that the case cannot happen, and fail loudly if it does.
default:
	panic(fmt.Sprintf("unhandled level %q", level))

Placing default first or last keeps it where readers look for it — see go.switch_default_first_or_last.

Configuration

Property Meaning Default

checkTypeSwitches

Also report a type switch (switch v := x.(type)) with no default. Off by default: a closed set of types handled exhaustively is normal Go, and the compiler offers no exhaustiveness guarantee a rule could lean on.

false

Four further shapes are excluded by construction.

The expression-less switch. switch { case a > b: …​ } is Go’s if / else if ladder written as a switch, not a dispatch on a value. Its missing catch-all is the concern of go.else_if_without_else; reporting it here would report one defect twice, differing only in syntax.

A bool switch covering both values. When the cases are exactly true and false, they already enumerate every value the type has and a default would be unreachable. A case that merely passes a boolean to a call — case allowed(true): — says nothing about exhaustiveness and is still reported.

A switch with no clauses. switch n {} is an empty statement with nothing to dispatch.

A switch whose every case terminates, with a statement after it. When every case ends in return, break, continue, goto, panic(…​) or os.Exit(…​), the statement following the switch is reached only when no case matched — it is the default, written unindented:

func (m mode) String() string {
	switch m {
	case active:
		return "active"
	case inactive:
		return "inactive"
	}

	return "unknown"                    // OK — this is the default
}

That is the idiomatic Go early return, the shape go.superfluous_else actively asks for and the one go.else_if_without_else exempts for the same reason; all three decide what "terminates" means the same way.

The statement does not have to be the switch’s own sibling. The catch-all is as often written after the construct that holds the switch — a switch that is the last statement of an if block, or of a case of an outer switch, with the return at the end of the function:

func serviceName(group, version string) (string, error) {
	switch group {
	case "extensions":
		return "legacy", nil
	case "networking.k8s.io":
		switch version {                // OK — the default is the return below
		case "v1beta1":
			return "beta", nil
		case "v1":
			return "stable", nil
		}
	}

	return "", errors.New("unresolved")
}

An enclosing for counts too: falling out of the switch runs the next iteration, which is where the unmatched value goes even when nothing follows the loop. A statement following an enclosing function literal does not count — it runs after the whole call that took the literal, not after the unmatched value inside it.

A catch-all is required: a terminating switch with nothing after it anywhere in its function has none, and an unmatched value really does fall out of the dispatch, so that shape is still reported.

© 2026 Xygeni. All rights reserved