AlgoMaster Logo

Switch Statement

Last Updated: May 17, 2026

11 min read

When you have a value that can take one of several known forms (an order status, a product category, a shipping tier) and you need a different action for each, an if/else if ladder works but quickly turns into a wall of conditions. The switch statement is the cleaner alternative: list the candidates, give each one a body, and let the compiler arrange the dispatch. This lesson covers the classic block form of switch in C#, including the fall-through rule, the default label, intentional jumps with goto case, and the pattern guards added in C# 7.

Why switch Exists

Imagine a checkout flow that routes an order to a different handler depending on its status. With if/else if, the code reads like this:

It works, but the same variable status is compared four times and the structure (one value, several candidates) is buried in the syntax. The switch statement makes the structure explicit:

Same behavior, but the shape of the code matches the shape of the problem: one input, a list of cases, one branch runs. The compiler is also free to compile a switch into a faster dispatch (a jump table or a hash lookup, depending on the cases) when the values cooperate.

Anatomy of a switch Block

A switch statement has three pieces: the value you're switching on, one or more case labels with bodies, and an optional default label.

A few rules from the start:

  • The expression in parentheses is evaluated once, then matched against the case labels in order.
  • Each case label is followed by a constant value, a comma is not allowed (one value per label).
  • The body of a case can be one or many statements. You don't need braces around them.
  • Every case body must end with a jump statement. The usual choice is break, which exits the switch. Other options exist (return, throw, goto), and we'll cover them below.

The expression's type drives what kinds of constants the case labels can use. Integral types (int, long, byte, short), char, string, bool, and enum types all work out of the box. Constants must be compile-time constants, you can't use a variable on the right of case.

The break Requirement (No Implicit Fall-Through)

C# inherits a lot of syntax from C and C++, but on one point it deliberately differs: case bodies don't fall through to the next case automatically. Each non-empty case must end with a jump statement, or the compiler refuses to build your program.

The compiler reports CS0163: Control cannot fall through from one case label ('case 1:') to another. In C, that fall-through is allowed and is a famous source of bugs. C# closes the loophole by requiring every non-empty case to explicitly say where it ends.

The standard exit is break:

break jumps out of the switch and continues with the statement after it. That's the right choice 95% of the time.

Other valid endings:

  • return exits the entire method, not just the switch.
  • throw raises an exception, which also exits the switch.
  • goto case X; jumps to another case in the same switch (covered below).
  • goto default; jumps to the default label.
  • continue jumps to the next iteration of a surrounding loop. Legal but rare.

The point of the rule is that the reader can look at the bottom of a case body and immediately know what happens next: end of switch, end of method, exception, or jump to a labeled target. Nothing implicit.

The One Allowed Fall-Through: Empty Cases

The strict rule has one practical relaxation: an empty case is allowed to fall through to the next label. This lets you group several values that share a body without repeating the code.

The three labels above the first break share one body. Whether the input is "novel", "biography", or "cookbook", control lands on the same Console.WriteLine call.

The rule is precise: a case with no statements between its label and the next label is allowed to fall through. The moment a case has any statement, it must end with a jump.

The novel case isn't empty here. It has a Console.WriteLine, so it needs its own break.

The default Label

default is the catch-all. If none of the case labels match, control jumps to default. It's how you handle the "anything else" branch.

A few details worth pinning down:

  • default is optional. A switch with no matching case and no default simply does nothing.
  • default doesn't have to appear last. The compiler matches cases by value, not by source order, so default can sit anywhere in the block. Style guides put it at the bottom because that's what readers expect.
  • Like every other case, the default body needs its own jump statement (break, return, throw).
  • You can have at most one default label per switch. A duplicate is a compile error.

When the value is an enum, it's a good habit to include default even if you think you've covered every member. Someone might add a new enum value later, and a missing case will quietly route to default instead of doing the wrong thing silently:

The default branch catches Cancelled because the explicit cases don't cover it. If a future commit adds Refunded to the enum, this code keeps compiling and the default keeps the program safe.

Dispatching by Case: A Visual

The switch block dispatches the value to exactly one matching case, runs that case's body, and exits via break (or another jump). Here's the flow for the order-status example, with each case colored by its outcome.

The decision node status value? is where the dispatch happens. Every case body funnels into the same end point, which is the statement after the switch block. The default branch (here, other) keeps the diagram honest: there's always a path out, even if the input is unexpected.

Intentional Fall-Through with goto case

Sometimes you genuinely want one case to continue into another. Empty case stacking handles the simple "these values share a body" pattern, but it doesn't help when each case has its own body and one case needs to also run the work of another.

C# uses goto case <value>; (or goto default;) for this. It's an explicit jump that says "after my body, continue at this other label."

Reading top to bottom: priority is "premium", so control enters the premium case, prints "Free express shipping.", and then goto case "standard" jumps to the standard case body, which prints "Sending receipt." and breaks. If the input had been "platinum", all three lines would print in order.

goto default; works the same way:

A few rules to keep goto case honest:

  • The target label must exist in the same switch. You can't goto case into another switch.
  • The target value must be a constant that matches an existing case label exactly. goto case "Premium"; fails if the label is case "premium": (string comparisons are case-sensitive).
  • goto case <value>; (jumping to another case in this switch) and goto default; are separate features from the general goto Label; statement.

Use goto case sparingly. When you find yourself reaching for it often, the cases probably share enough behavior to be factored into a helper method instead.

Switching on Strings

C# allows switch on string values directly. The match is a plain equality check, the same as == on strings (case-sensitive, ordinal comparison).

Two things to watch for:

  • null is a valid case label for strings (case null:). If the input string is null and you have no case null: and no default, the switch does nothing. If you have a default but no case null:, the null falls into default.
  • String matching is case-sensitive. "Card" does not match case "card":. If you need case-insensitive dispatch, normalize the input first (method = method.ToLowerInvariant();) or use a switch expression with a when clause that calls Equals with a comparer.

Old language versions (before C# 7) limited switch to integral types, char, string, bool, and enums. Modern C# (7+) opened it up to any type through patterns, which we'll glance at next.

A Light Touch of Patterns: Type Matching and when

C# 7 turned switch into a pattern-matching construct. The classic constant labels still work, but cases can also test the type of a value, bind it to a variable, and add extra conditions with a when clause. Here's just enough to read modern switch code.

A type pattern matches when the value is of a specific type, and binds it to a name in that case body:

payment is declared as object, but its runtime value is a decimal. The first case matches the type, binds the value to the local name amount, and the body uses it as a decimal without a manual cast.

A when clause adds a runtime condition. The case matches only when the pattern matches and the when expression is true:

Reading top to bottom: stock is 3. The case 0 constant doesn't match. The next case binds 3 to n and checks n < 5, which is true, so it runs. The remaining cases are skipped.

With when clauses, the order of cases starts to matter. Constant cases are checked first, but two when-guarded cases run in source order. The compiler will warn you (CS8120) when an earlier case makes a later one unreachable.

When to Choose switch Over if/else if

switch and if/else if overlap, but each has its sweet spot.

SituationBetter choice
Comparing one value against many constantsswitch
Dispatching on an enum or string from a fixed setswitch
Combining unrelated conditions (different variables, mixed comparisons)if/else if
Two or three branchesEither works, pick what reads cleaner
Range checks like n < 5 and n < 20switch with when, or if/else if
Single boolean condition with elseif/else

The deeper rule: switch is for dispatch on one value. The cleaner the "this is one value with several known forms" story is, the more switch pays off. When the branches each look at different variables or mix comparisons, an if/else if ladder is honest about what's happening.

A practical example. Same logic, two styles:

That's a switch expression (the topic of the _Switch Expressions_ lesson), shown here for contrast. The classic switch block does the same job a little more verbosely. Either way, the structure is "one value, four candidates," which is the perfect shape for switch.

Now consider this, which mixes inputs:

Each branch looks at a different combination of tier, weight, and isMember. There's no single value being dispatched. Forcing this into a switch would obscure what it's doing. if/else if is right.

Common Mistakes

A short tour of the mistakes that bite people learning switch. Most of them are compile errors, which means C# tells you about them right away, but knowing the patterns saves time.

Forgetting `break` on a non-empty case:

The fix is to add break; after the first case. C/C++ developers reach for switch and instinctively skip break, then get caught by the compiler.

Duplicate case labels:

Two cases with the same constant is always an error. If you want one body for several values, stack the labels instead.

Variable on the right of `case`:

Case labels must be compile-time constants. To match against a variable, use a pattern (case int n when n == target:) or rewrite the dispatch as an if/else if chain.

Assuming case order matters for plain constants:

The compiler is free to reorder these (typically by emitting a jump table). For constant labels, source order has no effect on which case runs. Order only matters with when clauses, where guarded cases are evaluated in source order.

Mixing up case-sensitivity in string switches:

The fix is the normalization on the input, not on the case labels. If you put case "Card": and case "card": together in the same switch, you're inviting drift the first time someone changes one and not the other.

A Look Ahead: Switch Expressions

The switch block produces a side effect (it runs statements). When all you actually want is to produce a value based on the input, the syntax feels wordy: every case has a body that assigns to the same variable, every case ends with break, and the structure repeats itself.

C# 8 added the switch expression form, with => arms instead of case bodies and a single trailing semicolon. The _Switch Expressions_ lesson covers it in full, but here's a peek so you recognize it in modern code:

The classic switch block is still the right tool when each case does work (prints, calls methods, throws, mutates state). The expression form is for when each case picks a value. Both have their place, and they share the same matching rules.

Summary

  • A switch statement dispatches one value against a list of case labels, runs the matching body, and exits. It's the natural shape for "one input, several known outcomes" logic.
  • Case labels must be compile-time constants. Integral types, char, string, bool, and enums work directly. Other types and ranges are reachable through patterns and when clauses (C# 7+).
  • C# forbids implicit fall-through. Every non-empty case body must end with break, return, throw, goto case, or goto default. The one exception is empty cases stacked above a shared body.
  • The default label is the catch-all. It's optional, can sit anywhere in the block (style guides put it last), and is the safety net for unexpected values, including unhandled enum members added later.
  • Use goto case <value>; or goto default; for intentional fall-through.
  • Type patterns let a case match by runtime type and bind the value to a local name. A when clause adds a runtime condition. Cases with guards are evaluated in source order.
  • Choose switch when the logic dispatches one value against a known set. Choose if/else if when branches mix different variables or unrelated comparisons.
  • Common mistakes are caught by the compiler: missing break, duplicate labels, non-constant case values. The error codes (CS0163, CS0152, CS0150) tell you exactly what's wrong.