I like this idea a lot. When the <% ... %> syntax was introduced, it was clear that <% if ... %>/<% elseif ... %>/<% else %>/<% endif %> was meant to be only the first of multiple uses of the basic idea. IIRC, a case statement was explicitly mentioned as a likely future candidate.
A few years ago, a relatively senior manager at GigantiCorp (a different one than the current GigantiCorp where I work now, but damned similar) decided that the only solution to a fairly minor feature request in our system was to create a large domain-specific language that our business users could operate. And he would design that system himself; it would be up to us to implement it.
His language looked suspiciously like the COBOL he grew up in. A group of the more senior people on the team got together and decided to write – off hours – critiques of the design of that language to present to even more senior leadership. One of my sections was the case statement, which he insisted be called nothing but “EVALUATE” (yes, he spoke in capital letters) and below is a draft of that document (written off-hours, remember) shorn only of an introductory sentence and a concluding paragraph related to the monstrosity of a language design.
The final report seems lost to history. I really wish I had it. We did all these straightforward factual reporting, which made no editorial comments whatsoever, but which inexorably damned his approach. I miss absolutely nothing about that job, except for this group of senior developers who could pull something like this off.
We presented our findings to a senior VP. The project was scrapped, and the manager took early retirement. A co-worker fulfilled that minor feature request with three days’ work.
Case Statement Analysis (~2300 words)
Case Statements and Pattern Matching Across Programming Languages
Introduction
The “case statement” — a construct for branching on the value of an expression — is one of the oldest and most universal control-flow primitives in programming. It exists in essentially every general-purpose language, but the form it takes varies dramatically. Some languages (the C family) treat it as syntactic sugar for a computed jump, complete with fall-through behavior that has caused countless bugs. Others (Pascal, Ada, modern functional languages) treat each branch as a self-contained alternative with no implicit flow between cases. And in the last fifteen or twenty years, mainstream languages have increasingly absorbed ideas from the ML/Haskell tradition, replacing or supplementing simple value-dispatch with full pattern matching — destructuring data, binding variables, guarding on conditions, and matching by structural shape rather than just equality.
This report surveys how case-style constructs work across a representative sample of languages, with emphasis on modern usage but with a look back at COBOL, Fortran, and LISP for historical perspective.
The C Family: switch/case/default with Fall-Through
C established a template followed almost verbatim by C++, Java, JavaScript, C#, Go, PHP, and many others. The keywords are switch, case, default, and (critically) break. The defining quirk is fall-through: once execution enters a case label, it continues into subsequent cases unless explicitly halted with break (or return, throw, etc.). This is occasionally useful for grouping cases, but it is so error-prone that most modern style guides require a comment whenever fall-through is intentional, and several newer languages in the C family have abandoned it.
C’s switch is also restricted in what it can match against: integer (or integer-promoted) constants only. No strings, no ranges, no patterns. C++ inherited this limitation; Java relaxed it in Java 7 to allow strings, and modern Java (14+) introduced switch expressions with arrow syntax (->) that don’t fall through, plus pattern matching for types and records.
Go kept the keywords but reversed the default: cases do not fall through, and you must write fallthrough explicitly to opt in. Go also allows expressions in case labels, multiple values per case, and a “type switch” form (switch v := x.(type)) for runtime type discrimination.
C# historically required a break (or goto case) at the end of every non-empty case — fall-through is a compile error rather than a default. Recent versions added rich pattern matching: type patterns, property patterns, relational patterns, and switch expressions using =>.
JavaScript and PHP retain classic C-style fall-through. JavaScript’s switch uses strict equality (===).
Pascal, Ada, and the “No Fall-Through” Tradition
Pascal introduced case ... of ... end with no fall-through and explicit support for ranges (1..5:) and lists of values per branch. Ada continued and tightened this: every possible value of the discriminant must be covered (the compiler enforces exhaustiveness), and others serves as the default. There is simply no way to fall through; each when branch is a single statement.
This tradition — exhaustiveness checking, no fall-through, range and list support — turns out to anticipate much of what modern functional languages do.
Shell, SQL, and Other Niches
Bash uses case ... in ... esac with glob-style patterns separated by | and terminated by ;;. Variants ;& (fall through unconditionally) and ;;& (continue testing remaining patterns) were added later. The patterns are real shell patterns — *.txt, [0-9]* — making bash’s case unusually expressive for string dispatch.
SQL has two forms: a simple CASE expr WHEN val THEN ... ELSE ... END and a searched form CASE WHEN condition THEN ... ELSE ... END. It is an expression, not a statement, and is one of the few places SQL offers conditional logic in a portable way.
Old Languages: COBOL, Fortran, LISP
COBOL uses EVALUATE, introduced in COBOL-85. It is surprisingly powerful: you can evaluate multiple subjects at once, and each WHEN clause can list values, ranges, or TRUE/FALSE for arbitrary boolean conditions. There is no fall-through. WHEN OTHER is the default.
Fortran added SELECT CASE in Fortran 90. Each CASE lists values or ranges (CASE (1:10)); there is no fall-through; CASE DEFAULT handles the fallback. Earlier Fortran had only IF/GOTO and the now-deprecated computed/arithmetic GOTO, which was a primitive ancestor of switch.
LISP and its descendants take a different approach because everything is an expression. Common Lisp has case (and ecase/ccase for exhaustiveness), cond (a chain of test/result pairs), and typecase (dispatch on type). Scheme has case and cond. Clojure has case (constant-time dispatch on compile-time literals), cond, condp, and — in the spirit of modern times — core.match for full pattern matching. None of these involve fall-through; each branch is a self-contained expression that produces a value.
Modern Pattern Matching: ML, Haskell, Rust, Scala, Swift, Python, Elixir
The functional tradition reframes “case” entirely. Instead of dispatching on a single value, you match against the shape of data. Patterns can:
- bind variables (
Some(x) matches and binds x),
- destructure tuples, lists, records, and algebraic data types,
- carry guards (
when x > 0 / if x > 0),
- nest arbitrarily deeply,
- be checked for exhaustiveness by the compiler.
Haskell uses case ... of, with each branch a pattern followed by -> and an expression. Pattern matching also works directly in function definitions via multiple equations.
OCaml/F# use match ... with and |-separated patterns leading to ->.
Rust has match, with => between pattern and arm, mandatory exhaustiveness, range patterns (1..=5), guards (if cond), | for or-patterns, and _ as the wildcard. Rust also has if let and while let for single-pattern shorthand, and (stabilized 2024) let ... else for the early-return idiom.
Scala has match with case keywords; patterns include extractors via unapply, type patterns, and guards.
Swift uses switch with case keywords but in pattern-matching style — no fall-through by default (use fallthrough to opt in), exhaustiveness required, and full destructuring/binding/where-guard support. Swift’s switch is the unusual case of borrowing C-family keywords while delivering ML-family semantics.
Python added structural pattern matching in 3.10 with match/case. Patterns include literals, classes (with positional or keyword sub-patterns), sequences ([1, 2, *rest]), mappings, captures, | for or-patterns, as for binding, and if for guards. The wildcard is _. There is no fall-through.
Elixir (and Erlang) lean heavily on pattern matching: case, multi-clause function heads, and the = operator itself is a pattern match. Guards use when.
Kotlin uses when, which doubles as both switch-replacement and a more general conditional (no subject expression needed). It can match values, ranges (in 1..10), types (is String), and is exhaustive when used as an expression on a sealed type or enum.
Ruby has case ... when ... end, where when uses the case-equality operator === — letting you match ranges, classes, regexes, and lambdas. Ruby 3.0 added case/in for full structural pattern matching, alongside the older case/when.
Summary Table
| Language |
Keywords |
Syntax Snippet |
Notes |
| C |
switch, case, default, break |
switch (x) { case 1: ...; break; default: ...; } |
Integer constants only. Fall-through by default. |
| C++ |
switch, case, default, break, [[fallthrough]] |
switch (x) { case 1: [[fallthrough]]; case 2: ...; break; } |
Like C; C++17 added [[fallthrough]] attribute to silence warnings. |
| Java |
switch, case, default, break, yield, -> |
switch (x) { case 1, 2 -> "small"; default -> "big"; } |
Modern arrow form: no fall-through, expression-valued, pattern matching on types/records (Java 21+). |
| C# |
switch, case, default, break, when, => |
var s = x switch { 1 => "one", > 10 => "big", _ => "other" }; |
No implicit fall-through (compile error). Switch expressions, type/property/relational patterns, when guards. |
| Go |
switch, case, default, fallthrough |
switch x { case 1, 2: ...; case 3: fallthrough; default: ... } |
No fall-through by default; fallthrough is opt-in. Type switch: switch v := x.(type). Cases can be expressions. |
| JavaScript |
switch, case, default, break |
switch (x) { case 1: ...; break; default: ...; } |
Fall-through by default. Strict equality (===). |
| TypeScript |
same as JS |
same as JS, plus exhaustiveness via never |
No native pattern matching; idiom is to assign to a never-typed variable in default for exhaustiveness checking. Proposal in flight. |
| PHP |
switch, case, default, break, match |
match($x) { 1, 2 => 'small', default => 'big' } |
Classic switch has fall-through and uses ==. PHP 8 added match expression: strict ===, no fall-through, exhaustive, returns a value. |
| Pascal |
case, of, else, end |
case x of 1: ...; 2..5: ...; else ... end; |
No fall-through. Ranges supported. |
| Ada |
case, is, when, others |
case X is when 1 | 2 => ...; when 3..5 => ...; when others => ...; end case; |
Exhaustiveness enforced by compiler. No fall-through. Ranges and | lists. |
| Bash |
case, in, esac, ;;, ;&, ;;& |
case $x in foo|bar) ...;; *.txt) ...;; *) ...;; esac |
Glob patterns. ;& falls through; ;;& continues testing. |
| SQL |
CASE, WHEN, THEN, ELSE, END |
CASE x WHEN 1 THEN 'one' WHEN 2 THEN 'two' ELSE 'other' END |
Expression, not statement. Simple and searched forms. |
| COBOL |
EVALUATE, WHEN, WHEN OTHER, END-EVALUATE |
EVALUATE X WHEN 1 ... WHEN 2 THRU 5 ... WHEN OTHER ... END-EVALUATE |
Multi-subject evaluation. Ranges via THRU. WHEN TRUE/WHEN FALSE for boolean conditions. No fall-through. |
| Fortran |
SELECT CASE, CASE, CASE DEFAULT, END SELECT |
SELECT CASE (X); CASE (1); ...; CASE (2:5); ...; CASE DEFAULT; ...; END SELECT |
Ranges via (low:high). No fall-through. |
| Common Lisp |
case, ecase, ccase, cond, typecase |
(case x (1 'one) ((2 3) 'small) (otherwise 'other)) |
Expression. ecase errors if no match (exhaustive). cond for arbitrary tests; typecase for type dispatch. |
| Scheme |
case, cond, else |
(case x ((1) 'one) ((2 3) 'small) (else 'other)) |
Like Common Lisp. Uses eqv? for comparison. |
| Clojure |
case, cond, condp, core.match/match |
(case x 1 "one", (2 3) "small", "other") |
case is constant-time, compile-time literals only. core.match adds full pattern matching. |
| Haskell |
case, of, ->, | (guards) |
case x of Just n -> n; Nothing -> 0 |
Full pattern matching with guards. Also via function-equation patterns. Exhaustiveness warned. |
| OCaml |
match, with, |, ->, when |
match x with | Some n when n > 0 -> n | _ -> 0 |
Patterns destructure variants, tuples, records, lists. Exhaustiveness checked. |
| F# |
match, with, |, ->, when |
match x with | Some n when n > 0 -> n | _ -> 0 |
Like OCaml. Active patterns add user-defined matchers. |
| Rust |
match, =>, _, if (guards), |, ..=, if let, while let, let ... else |
match x { 1..=5 => "small", n if n > 10 => "big", _ => "other" } |
Mandatory exhaustiveness. Range, or, binding, struct/enum/tuple patterns. if let for single-pattern shorthand. |
| Scala |
match, case, =>, _, if (guards) |
x match { case Some(n) if n > 0 => n; case _ => 0 } |
Extractor objects via unapply. Type patterns. Used heavily for ADTs. |
| Swift |
switch, case, default, where, fallthrough |
switch x { case 1...5: ...; case let n where n > 10: ...; default: ... } |
No fall-through unless fallthrough. Exhaustive. Pattern matching with binding, ranges, tuples, type casts via as, where guards. |
| Python |
match, case, _, |, as, if (guards) |
match p: case Point(x=0, y=0): ...; case Point(x, y) if x == y: ...; case _: ... |
Structural pattern matching since 3.10. Class, sequence, mapping, capture patterns. No fall-through. |
| Kotlin |
when, is, in, else, -> |
when (x) { 1, 2 -> "small"; in 3..10 -> "mid"; is String -> "str"; else -> "other" } |
Doubles as general conditional (no subject needed). Exhaustive on sealed/enum when used as expression. |
| Ruby |
case, when, in, then, else, end |
case x; when 1..5 then "small"; when String then "str"; else "other"; end |
when uses === (matches ranges, classes, regexes). Ruby 3.0+ adds case/in for full pattern matching with destructuring. |
| Elixir |
case, do, end, ->, when |
case x do {:ok, n} when n > 0 -> n; _ -> 0 end |
Pattern matching pervasive. Multi-clause function heads are the more idiomatic dispatch. Guards via when. |
| Erlang |
case, of, end, ;, ->, when |
case X of {ok, N} when N > 0 -> N; _ -> 0 end. |
Same family as Elixir. Guards restricted to a small set of pure functions. |
Observations
A few patterns emerge from this survey. First, fall-through is a historical accident of C that is steadily being designed away — Go made it opt-in, C# made it an error, and the entire ML/Haskell/Rust lineage rejects the very idea. Second, modern languages converge on three features: exhaustiveness checking, no fall-through, and structural pattern matching. Even languages that started out with simple C-style switches (Java, Python, Ruby, JavaScript via proposal) are growing toward this model. Third, the boundary between “case statement” and “pattern match” has effectively dissolved in current-generation languages: what Rust calls match and what Python calls match are the same construct, and they subsume what older languages split across switch, if-chains, and type tests. The case statement is no longer about jumping to a label; it is about decomposing data.