Switch Case in C#: Statement, Expression and Patterns (.NET 10)

Sooner or later you’re branching on one value with five possible answers. A switch case in C# matches that value against case labels and runs the first section that fits, which reads far better than a stack of else if lines.

In this tutorial, I’ll cover the switch case in C# as I use it on .NET 10, from the classic statement with break to pattern matching and the switch expression. You’ll also see the compiler errors each mistake produces.

C# Switch Case Syntax: switch, case, break and default

A switch statement evaluates its expression once, compares the result with each case label from top to bottom, and runs the statements of the first match. If nothing matches, the default section runs:

int day = 3;

switch (day)
{
    case 1:
        Console.WriteLine("Monday: team standup");
        break;
    case 3:
        Console.WriteLine("Wednesday: sprint review");
        break;
    case 5:
        Console.WriteLine("Friday: deploy freeze");
        break;
    default:
        Console.WriteLine("No meeting today");
        break;
}

Output:

Wednesday: sprint review
Switch case in C# statement with case 1, case 3, case 5, break and default, running in VS Code
day is 3, so only the case 3 section runs

You can switch on integers, char, string, bool, enums and, with patterns, any type at all. My guide to data types in C# lists the built-in ones.

Why Doesn’t C# Allow Fall-Through Between Cases?

In C and JavaScript, a missing break silently runs the next case too. C# turns that bug into a compile error. Here’s what happens when I forget the break after case 1:

int plan = 2;

switch (plan)
{
    case 1:
        Console.WriteLine("Basic plan");
    case 2:
        Console.WriteLine("Pro plan");
        break;
}

Build output:

C:\DotNetDemo\SwitchCase\FallThrough.cs(5,5): error CS0163: Control cannot fall through from one case label ('case 1:') to another

The build failed. Fix the build errors and run again.

Every switch section has to end in a jump: break, return, throw, goto case, goto default, or continue when the switch sits inside a loop like the ones in my while loop in C# guide.

Multiple Case Labels and goto in a C# Switch Statement

Fall-through isn’t allowed, but stacking labels on one section is. When you really want one case to continue into another, say so with goto case or goto default:

foreach (var state in new[] { "WA", "OR", "HI", "TX" })
{
    Console.Write($"{state}: ");
    switch (state)
    {
        case "WA":
        case "OR":
            Console.WriteLine("local, next-day");
            break;
        case "AK":
        case "HI":
            Console.Write("air freight, then ");
            goto default;
        default:
            Console.WriteLine("ground, 5 days");
            break;
    }
}

Output:

WA: local, next-day
OR: local, next-day
HI: air freight, then ground, 5 days
TX: ground, 5 days
C# switch statement with multiple case labels WA and OR sharing one section and goto default for AK and HI, VS Code output
WA and OR share a section; HI jumps to default with goto

Since C# 9 you can also write the stacked labels as one pattern, case "WA" or "OR":. For other ways to test one value against a list, see compare a string with multiple values in C#.

Switch Case with Strings and Enums in C#

Switching on a String

String cases compare with ordinal equality, so they’re case-sensitive: "Yes" doesn’t match case "yes". I normalize user input once with Trim().ToUpperInvariant() and write the labels in uppercase:

string[] replies = ["yes", "Yes", " NO ", "maybe"];

foreach (string raw in replies)
{
    switch (raw.Trim().ToUpperInvariant())
    {
        case "YES":
            Console.WriteLine($"'{raw}' -> confirmed");
            break;
        case "NO":
            Console.WriteLine($"'{raw}' -> cancelled");
            break;
        default:
            Console.WriteLine($"'{raw}' -> unknown");
            break;
    }
}

Output:

'yes' -> confirmed
'Yes' -> confirmed
' NO ' -> cancelled
'maybe' -> unknown

ToUpperInvariant() avoids culture surprises such as the Turkish dotted i.

Switching on an Enum

Enums are the most natural fit for a switch, because the labels are a closed list of named values. You can read more in my enum in C# tutorial:

var status = OrderStatus.Shipped;

switch (status)
{
    case OrderStatus.Pending:
        Console.WriteLine("Waiting for payment");
        break;
    case OrderStatus.Shipped:
        Console.WriteLine("On its way via UPS");
        break;
}

enum OrderStatus { Pending, Shipped }

Output:

On its way via UPS

A switch statement doesn’t force you to cover every member. Add Returned later and this code quietly ignores it; a switch expression would warn you.

Pattern Matching in C# Switch Case Labels

Case labels aren’t limited to constants. You can test the type of the value, compare it with >= or <, and add a when guard for anything a pattern can’t express:

object?[] grades = [97, 54, 81, "B+", null, true];

foreach (object? g in grades)
{
    string text;
    switch (g)
    {
        case >= 90: text = "honor roll"; break;
        case int s when s < 60: text = $"failing ({s})"; break;
        case int s: text = $"passing ({s})"; break;
        case string letter: text = $"letter {letter}"; break;
        case null: text = "no grade yet"; break;
        default: text = $"unsupported {g.GetType().Name}"; break;
    }
    Console.WriteLine($"{g ?? "null"}: {text}");
}

Output:

97: honor roll
54: failing (54)
81: passing (81)
B+: letter B+
null: no grade yet
True: unsupported Boolean
C# switch case pattern matching with relational case >= 90, type patterns, when guard and case null, output in VS Code
Relational, type, when-guard and null cases over an object array

case int s checks the type and gives you a typed variable in one step, with no cast like the ones in type casting in C#. Order matters: the when s < 60 case must come before the plain case int s.

The C# Switch Expression: => Arms and the _ Discard

When each branch just produces a value, the switch expression (C# 8 and later) is shorter. The input comes first, each arm is pattern => result, and _ catches everything else:

int[] scores = [98, 85, 72, 64, 41];

foreach (int score in scores)
{
    string letter = score switch
    {
        >= 90 => "A",
        >= 80 => "B",
        >= 70 => "C",
        >= 60 => "D",
        _ => "F"
    };
    Console.WriteLine($"{score} -> {letter}");
}

Output:

98 -> A
85 -> B
72 -> C
64 -> D
41 -> F
C# switch expression with relational patterns >= 90, >= 80 and the _ discard converting scores to letter grades in VS Code
A switch expression turns each score into a letter grade

Arms are checked top to bottom, so >= 80 only sees scores under 90. Pair it with the foreach loop in C# like this and you’ve got a grading report in a dozen lines.

What Happens When No Switch Arm Matches?

Leave out the discard and you get warning CS8509, but it still builds. An unmatched input then throws:

Console.WriteLine(Fee("CA"));
Console.WriteLine(Fee("NY"));

static string Fee(string state) => state switch
{
    "CA" => "$12.50",
    "TX" => "$9.75"
};

Output:

C:\DotNetDemo\SwitchCase\NoMatch.cs(4,42): warning CS8509: The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern '""' is not covered.
$12.50
Unhandled exception. System.Runtime.CompilerServices.SwitchExpressionException: Non-exhaustive switch expression failed to match its input.
Unmatched value was NY.
   at <PrivateImplementationDetails>.ThrowSwitchExpressionException(Object unmatchedValue)
   at Program.<<Main>$>g__Fee|0_0(String state) in C:\DotNetDemo\SwitchCase\NoMatch.cs:line 4
   at Program.<Main>$(String[] args) in C:\DotNetDemo\SwitchCase\NoMatch.cs:line 2

On .NET (Core 3.0 and later) the exception is SwitchExpressionException; on .NET Framework it’s InvalidOperationException. Don’t wrap it in try-catch; add a _ arm that returns a fallback or throws a clear ArgumentOutOfRangeException.

Property and Tuple Patterns in a C# Switch Expression

A property pattern looks inside an object. This shipping rule checks membership first, then the state, then the order total:

using System.Globalization;
CultureInfo.CurrentCulture = new("en-US");

Order[] orders = [new("Jenna Moore", "CA", 120m, true),
    new("Ryan Carter", "TX", 35m, false),
    new("Chloe Brady", "AK", 80m, false)];
foreach (var o in orders)
{
    decimal ship = o switch
    {
        { IsMember: true } => 0m,
        { State: "AK" or "HI" } => 24.99m,
        { Total: >= 50m } => 0m,
        _ => 7.99m
    };
    Console.WriteLine($"{o.Name} ({o.State}): {ship:C}");
}
record Order(string Name, string State, decimal Total, bool IsMember);

Output:

Jenna Moore (CA): $0.00
Ryan Carter (TX): $7.99
Chloe Brady (AK): $24.99
C# switch expression property pattern on an Order record checking IsMember, State AK or HI and Total over 50, USD output in VS Code
Property patterns pick a shipping fee for each order

I set en-US because :C follows the machine’s culture. To switch on two values at once, wrap them in a tuple and use positional patterns:

Console.WriteLine(Winner("rock", "scissors"));
Console.WriteLine(Winner("paper", "scissors"));
Console.WriteLine(Winner("rock", "rock"));

static string Winner(string p1, string p2) => (p1, p2) switch
{
    var (x, y) when x == y => "tie",
    ("rock", "scissors") or ("paper", "rock")
        or ("scissors", "paper") => "player 1 wins",
    _ => "player 2 wins"
};

Output:

player 1 wins
player 2 wins
tie

The when x == y arm catches a tie first. More on tuple in C#.

Combining Switch Patterns with and, or and not

C# 9 added and, or and not for patterns. not binds first, then and, then or, so I add parentheses whenever I mix them:

foreach (char c in "a7Z# ")
{
    string kind = c switch
    {
        (>= 'a' and <= 'z') or (>= 'A' and <= 'Z') => "letter",
        >= '0' and <= '9' => "digit",
        not ' ' => "symbol",
        _ => "space"
    };
    Console.WriteLine($"'{c}' is a {kind}");
}

Output:

'a' is a letter
'7' is a digit
'Z' is a letter
'#' is a symbol
' ' is a space

The same and/or thinking shows up in a leap year check in C#, where divisibility rules combine.

Common C# Switch Case Compiler Errors

This file packs three common switch mistakes into one build:

int code = 404;

switch (code)
{
    case 200: Console.WriteLine("OK"); break;
    case 200: Console.WriteLine("Again"); break;
    case > 100: Console.WriteLine("Big"); break;
    case 404: Console.WriteLine("Not found"); break;
}

string label = code switch
{
    _ => "any",
    404 => "not found"
};

Build output:

C:\DotNetDemo\SwitchCase\SwitchErrors.cs(6,5): error CS0152: The switch statement contains multiple cases with the label value '200'
C:\DotNetDemo\SwitchCase\SwitchErrors.cs(8,10): error CS8120: The switch case is unreachable. It has already been handled by a previous case or it is impossible to match.
C:\DotNetDemo\SwitchCase\SwitchErrors.cs(14,5): error CS8510: The pattern is unreachable. It has already been handled by a previous arm of the switch expression or it is impossible to match.

The build failed. Fix the build errors and run again.
CodeWhat it meansFix
CS0163A section falls through to the next caseEnd it with break, return, throw or goto
CS0152Two cases use the same constantDelete or merge the duplicate
CS8120A case is unreachable (an earlier case covers it)Move the specific case above the general one
CS8510A switch expression arm is unreachable, often after _Keep the discard arm last
CS8509 (warning)A switch expression isn’t exhaustiveAdd a _ arm

Switch vs If-Else in C#: Which Should You Use?

SituationMy pick
One value, several fixed answersSwitch expression
One value, several blocks of statementsSwitch statement
Ranges or types of one valueSwitch with relational or type patterns
Conditions on several unrelated variablesif / else if
Lookups that change at run timeA dictionary

For two conditions an if is clearer, as in finding the largest of three numbers. Once the labels become data, such as prices loaded from a file, a dictionary in C# beats hard-coded cases.

Want more practice with control flow? These are good next steps:

Frequently Asked Questions

Is break required in a C# switch case?

Every switch section must end with a jump statement. break is the usual one, but return, throw, goto case and goto default also work. The default section needs one too.

Can you switch on a string in C#?

Yes. String cases use ordinal, case-sensitive comparison, so normalize the input first with something like Trim().ToUpperInvariant() and write the labels to match.

What is the difference between a switch statement and a switch expression?

A switch statement runs blocks of statements; a switch expression returns a value from the first matching arm and has no break or case keywords. See the switch expression reference on Microsoft Learn.

How do you use multiple values in one switch case?

Stack the labels (case "WA": case "OR":) over one section, or since C# 9 combine them with an or pattern: case "WA" or "OR":.

Can a C# switch case check a range like >= 90?

Yes, with relational patterns from C# 9: case >= 90: in a statement or >= 90 => "A" in an expression. Combine bounds with and, for example >= 80 and < 90. Details are in the patterns reference on Microsoft Learn.

Where should the default case go?

Anywhere in the switch statement. It runs only when no other case matches, whatever its position, but most teams keep it last for readability.