← Open in the full interactive course (progress tracking, search & more)

A classic switch statement tells the computer what to do. A switch expression tells it what value to produce — and that small shift changes everything about how safe and concise it can be.

You already learned switch statements back in Control Flow, and you just spent the last lesson learning how rich a single pattern (is Circle { Radius: > 10 }) can be. Now imagine combining the two: what if switch could test a whole series of patterns, one after another, and directly produce a value — instead of just running statements and hoping every branch remembers to assign one?

That's exactly what a switch expression is. It takes everything you learned about patterns in the last lesson and puts them to work choosing between values, as compactly and safely as C# gets.

In this lesson, you'll learn switch expressions (=>), how they differ from classic switch statements, pattern-based switch arms, when guards for extra conditions, and how the compiler checks your cases for exhaustiveness — plus the discard _ arm that catches everything else.

What Is It?

The Simple Explanation

A switch expression is a compact way to say "look at this value, find the first matching case, and give me back whatever that case says." Unlike a classic switch statement — which runs a block of statements per case — a switch expression's entire job is to evaluate to a single value, like a supercharged ternary operator that can handle many cases instead of just two.

The Technical Definition

A switch expression (C# 8) is an expression-form of switch: value switch { pattern1 => result1, pattern2 => result2, ... }. Each arm pairs a pattern (any of the patterns from the previous lesson — type, property, relational, or a combination) with a result expression, connected by =>. Arms are tried top-to-bottom; the first one whose pattern matches supplies the expression's value, and evaluation stops there. An optional when guard adds an extra boolean condition to a pattern. The compiler performs exhaustiveness analysis: if it can determine that not every possible input is covered, it emits a warning (and, since no arm matched at runtime for an uncovered case, throws a SwitchExpressionException) — which is why a final discard arm (_ => ...) is so commonly used to guarantee every case is handled.

switch statement

switch expression

Why Does It Exist?

The Problem

Assigning a value based on several conditions with a classic switch statement is more ceremony than the logic deserves:

decimal discountRate;
switch (customerTier)
{
    case "Bronze":
        discountRate = 0.02m;
        break;
    case "Silver":
        discountRate = 0.05m;
        break;
    case "Gold":
        discountRate = 0.10m;
        break;
    default:
        discountRate = 0m;
        break;
}

Fourteen lines, and the risk of a real bug lurking in it: if you forget to assign discountRate in one branch (or forget a break and accidentally fall through), the compiler may not catch it until much later, if at all.

The Need

Developers needed a form of switch whose entire purpose is producing a value — where the compiler can verify every branch actually returns something of the right type, where there's no break to forget, and where the new pattern-matching vocabulary (type, property, relational patterns) could be used directly as the case labels.

The Solution

The switch expression collapses the same logic into a direct assignment:

decimal discountRate = customerTier switch
{
    "Bronze" => 0.02m,
    "Silver" => 0.05m,
    "Gold" => 0.10m,
    _ => 0m
};

Every arm's expression must produce a compatible type, there's no break to forget, and the compiler checks that you've covered your bases.

Big Picture

STATEMENT vs EXPRESSION — WHAT EACH ONE IS FOR
switch (x) { case 1: DoThis(); break; ... } — "do a series of actions"

var result = x switch { 1 => "one", ... }; — "pick a single value"

How It Works

SWITCH EXPRESSIONS — STEP BY STEP
1. BASIC SHAPE — value switch { pattern => result, ... }
string sizeLabel = weightKg switch
{
    < 1 => "Envelope",
    < 5 => "Small",
    < 20 => "Medium",
    _ => "Large"
};
2. USE ANY PATTERN AS A CASE — INCLUDING PROPERTY PATTERNS
decimal shippingCost = order switch
{
    { Destination: "International", WeightKg: > 20 } => 75m,
    { Destination: "International" } => 35m,
    { Destination: "Domestic", WeightKg: <= 5 } => 5m,
    { Destination: "Domestic" } => 12m,
    _ => 15m
};
3. ADD AN EXTRA CONDITION WITH when
string status = order switch
{
    { IsCancelled: true } => "Cancelled",
    { ShippedDate: not null } when order.ShippedDate > DateTime.UtcNow.AddDays(-2) => "Recently shipped",
    { ShippedDate: not null } => "Shipped",
    _ => "Processing"
};
4. EXHAUSTIVENESS AND THE DISCARD ARM
//  CS8509: the switch expression does not handle all possible values
string Describe(int score) => score switch
{
    >= 90 => "A",
    >= 80 => "B",
    >= 70 => "C"
    // missing a case for anything below 70!
};

//  The discard arm _ guarantees every remaining value is handled
string Describe(int score) => score switch
{
    >= 90 => "A",
    >= 80 => "B",
    >= 70 => "C",
    _ => "F"
};

Simple Example

string ClassifyGrade(int score) => score switch
{
    < 0 or > 100 => throw new ArgumentOutOfRangeException(nameof(score)),
    >= 90 => "A",
    >= 80 => "B",
    >= 70 => "C",
    >= 60 => "D",
    _ => "F"
};

Console.WriteLine(ClassifyGrade(95)); // "A"
Console.WriteLine(ClassifyGrade(50)); // "F"

Code → Meaning → Result: Each arm reads as a direct rule ("90 or above is an A"), arms are checked top to bottom so the highest threshold that matches wins, and a switch arm can even throw as its result expression — useful for turning invalid input into an immediate, clear error as part of the same expression.

Real-World Example

A discount-tier calculator for an e-commerce order, combining a property pattern, a when guard, and a guaranteed fallback:

public record Customer(string Tier, int YearsAsMember, decimal LifetimeSpend);

decimal GetDiscountRate(Customer customer) => customer switch
{
    { Tier: "Platinum" } => 0.20m,
    { Tier: "Gold", YearsAsMember: >= 5 } => 0.15m,
    { Tier: "Gold" } => 0.10m,
    { LifetimeSpend: > 10_000m } when customer.YearsAsMember >= 2 => 0.08m,
    { Tier: "Silver" } => 0.05m,
    _ => 0.0m
};

var customer = new Customer("Gold", YearsAsMember: 6, LifetimeSpend: 4200m);
Console.WriteLine(GetDiscountRate(customer)); // 0.15 — Gold AND 5+ years wins over plain Gold

Notice the order of the arms matters: { Tier: "Gold", YearsAsMember: >= 5 } is checked before the plain { Tier: "Gold" } arm, so a long-standing Gold member gets the better rate — if the two arms were reversed, the more general one would always win first and the specific rule would never be reached. This is exactly the "first match wins" behavior to keep in mind when ordering your arms from most-specific to least-specific.

Under the Hood

HOW ARMS ARE EVALUATED
SEQUENTIAL PATTERN TESTS, JUST LIKE if/else if

A switch expression doesn't use a jump table the way a classic integer switch statement sometimes can — because patterns are far richer than simple constant comparisons, the compiler generally compiles a switch expression into a sequence of pattern tests, tried in the order you wrote them, functionally similar to a chain of if/else if checks. (For the simplest cases — matching plain constants of a primitive type — the compiler can and does optimize toward a jump table, just like a classic switch statement would.) An unmatched value at runtime, when the compiler couldn't prove exhaustiveness, throws System.Runtime.CompilerServices.SwitchExpressionException.

Common Confusion

1. Switch expression syntax vs switch statement syntax — don't mix them up

A switch statement uses case value: and needs break; a switch expression uses pattern => and has no case or break at all, using commas between arms instead. They look related but the syntax genuinely doesn't interchange — you can't add a break to a switch expression arm, and you can't use => in a switch statement's case label.

2. "First match wins" — order matters, especially with overlapping patterns

If two arms could both match the same value, whichever comes first in the expression is the one that fires — the rest are never even checked for that value. This is why the real-world example above lists the more specific { Tier: "Gold", YearsAsMember: >= 5 } arm before the more general { Tier: "Gold" } arm.

3. A compiler warning about exhaustiveness isn't the same as a guarantee against crashes

The compiler's exhaustiveness check is best-effort static analysis — for patterns based on runtime property values (like { Tier: "Gold" }) it generally can't prove every possible string is covered, so it won't warn even though a truly unexpected Tier value could still fall through with no matching arm and throw at runtime. Always include a discard (_) arm unless you are certain every case is genuinely and provably covered.

Common Mistakes

Mistake 1 — Omitting the discard arm and getting a runtime crash later

Assuming a set of specific-value arms covers everything, without a final _:

string label = status switch
{
    "Active" => "",
    "Inactive" => ""
    // any other string throws SwitchExpressionException at runtime
};

Add _ => "" (or a deliberate, informative throw) as the final arm, so unexpected values are handled explicitly and predictably.

Mistake 2 — Ordering arms so a general pattern shadows a specific one

decimal rate = customer switch
{
    { Tier: "Gold" } => 0.10m,                       //  this matches first...
    { Tier: "Gold", YearsAsMember: >= 5 } => 0.15m,   // ...so this is unreachable!
    _ => 0.0m
};

List more specific patterns before more general ones that could also match the same value. Some IDEs and the compiler will even warn about an unreachable arm in obvious cases.

Mistake 3 — Reaching for a switch expression when a switch statement is actually the right tool

A switch expression is for producing a value. If what you actually need is to run several different side effects (log something, call a different service, update multiple pieces of state) per case, forcing that into a switch expression — often by discarding a return value or awkwardly stuffing statements into a local function — usually reads worse than a plain switch statement. Use the tool that matches the shape of what you're doing: value selection → switch expression; multi-step actions → switch statement.

When Should I Use It?

Mental Model

switch statement = "do one of these things"
switch expression = "become one of these values"
Arms are checked top to bottom — the first match wins, so order specific before general.
_ = "anything else" — your safety net against a runtime crash.

Remember: a switch expression is patterns (from the last lesson) plus value production — the two lessons are really one idea in two parts.

Key Takeaway


Check Your Understanding

You've seen how switch expressions combine everything from the pattern-matching lesson with direct value production. Let's check your understanding.

1. What is the most fundamental difference between a switch statement and a switch expression?

Show answer

Correct: B

Why B is correct: This is the core distinction. A switch statement is about control flow — running different code per case. A switch expression is about producing a value — every arm supplies a result of the expression's overall type.

Why A is incorrect: Both switch statements and switch expressions work with a wide range of types, not just strings.

Why C is incorrect: It's the reverse — switch expressions are actually where the richest pattern matching (property, relational, combinators) is most naturally used.

Why D is incorrect: They have genuinely different syntax and semantics — different keywords, different structure, and fundamentally different purposes (action vs. value).

Reinforcement: When you need to pick a value, reach for a switch expression; when you need to run different actions, reach for a switch statement.

2. Given this switch expression, what does GetDiscountRate return for a customer with Tier: "Gold", YearsAsMember: 6?

decimal GetDiscountRate(Customer c) => c switch
{
    { Tier: "Gold" } => 0.10m,
    { Tier: "Gold", YearsAsMember: >= 5 } => 0.15m,
    _ => 0.0m
};
Show answer

Correct: B

Why B is correct: Switch expression arms are evaluated top to bottom, and the first matching arm wins. Since { Tier: "Gold" } already matches this customer, it fires before the compiler ever gets to the more specific YearsAsMember: >= 5 arm — making that second arm effectively unreachable for any Gold customer.

Why A is incorrect: There's no automatic "most specific wins" behavior in C# switch expressions — order is exactly what determines which arm fires.

Why C is incorrect: The first arm does match — Tier is "Gold" — so the discard arm is never reached.

Why D is incorrect: An exception only occurs when no arm matches at all; here, the first arm matches successfully.

Reinforcement: Always order more specific patterns before more general ones that could also match the same value — this is a genuine, easy-to-make bug.

3. What happens at runtime if a switch expression has no arm matching the given value, and no discard (_) arm was provided?

Show answer

Correct: B

Why B is correct: When the compiler can't prove exhaustiveness, an unmatched value at runtime throws SwitchExpressionException. This is why the discard arm is such a common defensive habit — it guarantees a fallback rather than relying on the compiler's static analysis to catch every case in advance.

Why A is incorrect: There is no silent default fallback — an unmatched case is treated as an error condition, not quietly ignored.

Why C is incorrect: The compiler only produces a warning, not a hard error, in cases where it can't prove exhaustiveness (which is common for patterns based on runtime string or property values) — the code still compiles and can crash later at runtime.

Why D is incorrect: There's no such fallback behavior — an unmatched value is a genuine runtime error, not a silent default to the first arm.

Reinforcement: A discard arm isn't just stylistic — it closes a real gap the compiler can't always catch on its own.

4. When is a classic switch statement the better choice over a switch expression?

Show answer

Correct: B

Why B is correct: A switch expression's entire purpose is producing a value. If each case genuinely needs to run several statements or side effects (logging, multiple service calls, updating multiple pieces of state), a switch statement fits that shape far more naturally than contorting a switch expression to do it.

Why A is incorrect: Switch statements remain the right tool for action-oriented, multi-statement branching — the two forms serve different purposes, not a strict "old vs. new" replacement.

Why C is incorrect: Pattern matching works in both switch statements and switch expressions — this isn't a distinguishing factor between them.

Why D is incorrect: Performance is not a meaningful differentiator here — both compile to comparable branching logic, and for constant-based cases both can be optimized similarly by the compiler.

Reinforcement: Choose based on shape: producing one value → switch expression; performing multiple actions → switch statement.

You can now express rich, pattern-based decision logic as a single readable value expression instead of a scaffold of statements.


dotnetmadeeasy.com — Learn C# and .NET, the right way.