Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

C# & .NET (C# 14, .NET 10 LTS)  Module 3 – Control flow and methods

if, switch statements and switch expressions

Choose between if and else if, a switch statement and a switch expression in C#, with case guards, no fall-through and the warnings for a missing case.

  • Beginner
  • 15 minutes
  • Examples run with .NET 10.0.401 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 15 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Write if and else if chains whose tests run in the right order
  • Replace nested checks with guard clauses that return early
  • Write switch statements with several labels, case guards and goto case
  • Rewrite a value mapping as a switch expression with relational patterns
  • Predict the warning and the exception of a switch expression that misses a value

Before you start

What you’ll learn · Before you start

What you will learn

  • Write if and else if chains whose tests run in the right order
  • Replace nested checks with guard clauses that return early
  • Write switch statements with several labels, case guards and goto case
  • Rewrite a value mapping as a switch expression with relational patterns
  • Predict the warning and the exception of a switch expression that misses a value

Before you start

On this page

An income of 950,000 falls in one of five bands, and the program that taxes it must pick exactly one rate. Written with if and else if, that choice takes four tests in the right order; written as a switch expression, it takes five arms that read like the table of bands itself.

You will step through the tests C# makes, replace nested checks with guard clauses, write switch statements that never fall through, and see the warning and the exception that a switch expression gives when it misses a value. Each program is shown with the output it printed.

if and else if test their conditions in order, and the first true one wins

An if statement runs its block only when its condition is true, and its else block runs otherwise. Put another if in the else and you get a chain, else if. C# tests the conditions from the top, one at a time, stops at the first one that is true, runs that block and skips everything after it. This lesson uses an illustrative tax scheme with made-up bands, not any country’s law:

Income Rate
up to 250,000 0 %
250,001 to 600,000 5 %
600,001 to 1,100,000 10 %
1,100,001 to 2,000,000 20 %
above 2,000,000 30 %

Step through the chain for an income of 950,000:

Interactive · step by step How the if and else if chain picks the rate for 950,000 Step through the tests from the top. A test runs only when every test above it was false.
How the if and else if chain picks the rate for 950,000, step 6 of 6. Showing income <= 250,000? (false for 950,000), income <= 600,000? (false for 950,000), income <= 1,100,000? (true for 950,000), income <= 2,000,000?, else: rate 30 %, rate 0 %, rate 5 %, rate 10 %, rate 20 %. In focus: income <= 250,000?, rate 0 %.falsefalsefalsefalsetruetruetruetrueincome <= 250,000?false for 950,000income <= 600,000?false for 950,000income <= 1,100,000?true for 950,000income <= 2,000,000?else: rate 30 %rate 0 %rate 5 %rate 10 %rate 20 %true for 250,000

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. The first test. The chain starts at the top with an income of 950,000. 950,000 <= 250,000 is false, so the first block is skipped and the next test runs.
  2. The second test. Reaching this test already means the income is above 250,000, so it really asks whether the income is from 250,001 to 600,000. 950,000 is not.
  3. The third test. 950,000 <= 1,100,000 is true. This is the band: above 600,000, because both tests above failed, and at most 1,100,000.
  4. Its block runs. rate = 10 runs, and the chain is finished. bands.cs printed 10 % for 950,000.
  5. Nothing below runs. The fourth test and the else block are skipped without being evaluated: once one condition is true, C# leaves the chain.
  6. A boundary value. For an income of exactly 250,000 the first test is already true, because <= includes its boundary, so it pays 0 %. 250,001 fails that test and pays 5 %.

950,000 failed the first two tests and passed the third, so the rate is 10 % and the fourth test never ran. Because a test runs only when every test above it was false, the second test, income <= 600_000, really means “above 250,000 and at most 600,000”: the order of the tests carries half of each condition. The program below runs the chain on six incomes, the boundaries included, next to a version that tests in the wrong order:

bands.cs: one rate per income, from an if and else if chain C# · bands/bands.cs
// The rate of an illustrative tax scheme (made-up bands, not any country's law), picked with if and else if.
// RateFor tests the bands from the lowest up. RateWrongOrder tests "more than" from the lowest up, which is a bug.
int[] incomes = [180_000, 250_000, 250_001, 950_000, 1_100_000, 2_400_000];
foreach (int income in incomes)
{
    Console.WriteLine($"{income,9:N0}: {RateFor(income),2} %   wrong order: {RateWrongOrder(income),2} %");
}

static int RateFor(int income)
{
    int rate;
    if (income <= 250_000)
    {
        rate = 0;
    }
    else if (income <= 600_000)
    {
        rate = 5;
    }
    else if (income <= 1_100_000)
    {
        rate = 10;
    }
    else if (income <= 2_000_000)
    {
        rate = 20;
    }
    else
    {
        rate = 30;
    }
    return rate;
}

static int RateWrongOrder(int income)
{
    int rate = 0;
    if (income > 250_000)
    {
        rate = 5;
    }
    else if (income > 600_000)
    {
        rate = 10;
    }
    else if (income > 1_100_000)
    {
        rate = 20;
    }
    else if (income > 2_000_000)
    {
        rate = 30;
    }
    return rate;
}

Output

  180,000:  0 %   wrong order:  0 %
  250,000:  0 %   wrong order:  0 %
  250,001:  5 %   wrong order:  5 %
  950,000: 10 %   wrong order:  5 %
1,100,000: 10 %   wrong order:  5 %
2,400,000: 30 %   wrong order:  5 %

Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run bands.cs

<= puts each boundary in the lower band: 250,000 pays 0 % and 250,001 pays 5 %, as the first lines show. Decide for every boundary which band it belongs to before you write its test, because a boundary on the wrong side charges real people the wrong amount.

Testing the widest condition first

RateWrongOrder starts with if (income > 250_000), which is true for every income above 250,000, so the tests below it never run and 2,400,000 pays 5 %. Test from the lowest band up with <=, as RateFor does, or from the highest band down with >.

Checkpoint

Check: if and else if chains

  1. Question 1 of 2 With the bands of this lesson, which rate does RateFor give an income of exactly 600,000?

    Choose one answer.

    A hint

    Run the tests from the top and stop at the first one that is true. Does <= include the boundary?

    Show the answer: Question 1 of 2

    Answer: 5 %

    An if and else if chain runs only the block of the first true condition. For 600,000 that is the second test, income <= 600_000, so the rate is 5 %; the boundary belongs to the lower band because of <=.

  2. Question 2 of 2 What does this method return for 90 marks?

    Read the code, then choose one answer.

    static string Grade(int marks)
    {
        if (marks >= 35)
        {
            return "pass";
        }
        else if (marks >= 75)
        {
            return "distinction";
        }
        return "fail";
    }
    Show the answer: Question 2 of 2

    Answer: "pass"

    The first true condition wins, so a chain that tests "at least" values must start with the highest one: marks >= 75 first, then marks >= 35. Written the other way, every pass is a plain pass, the same bug that gave every income above 250,000 a rate of 5 %.

Guard clauses return early, so the main path stays flat

Every nested if pushes the code that does the real work one level deeper and its error message further from the test that produces it. A guard clause: A check at the start of a method that returns or throws at once when an input is bad, so the code after it can assume the input is good and stays unindented. turns that around: it tests for one bad input at the top of a method and leaves at once with return or throw, so the code below it can assume that case is ruled out. Here are the same three checks of an order, nested and written as guard clauses:

guards.cs: three checks, nested and as guard clauses C# · guards/guards.cs
// The same three checks written nested, and as guard clauses that return early. Both give the same answers.
(string? Item, int Quantity)[] orders = [(null, 2), ("", 2), ("pen", 0), ("pen", 3)];
foreach (var (item, quantity) in orders)
{
    string nested = Nested(item, quantity);
    string guarded = Guarded(item, quantity);
    string input = $"({(item is null ? "null" : $"\"{item}\"")}, {quantity})";
    Console.WriteLine($"{input,-12} {guarded,-28} same answer: {nested == guarded}");
}

static string Nested(string? item, int quantity)
{
    if (item is not null)
    {
        if (item.Length > 0)
        {
            if (quantity >= 1)
            {
                return $"{quantity} x {item}";
            }
            else
            {
                return "quantity must be at least 1";
            }
        }
        else
        {
            return "item is empty";
        }
    }
    else
    {
        return "item is missing";
    }
}

static string Guarded(string? item, int quantity)
{
    if (item is null)
    {
        return "item is missing";
    }
    if (item.Length == 0)
    {
        return "item is empty";
    }
    if (quantity < 1)
    {
        return "quantity must be at least 1";
    }
    return $"{quantity} x {item}";
}

Output

(null, 2)    item is missing              same answer: True
("", 2)      item is empty                same answer: True
("pen", 0)   quantity must be at least 1  same answer: True
("pen", 3)   3 x pen                      same answer: True

Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run guards.cs

Both methods gave the same answer for all four orders. Guarded reads from top to bottom: each test sits on the line above its message, and the line that does the work, return $"{quantity} x {item}";, is not indented at all. In Nested the message for a missing item is the method’s last return, 20 lines below the test that leads to it, and the working line sits three blocks deep.

A guard clause also documents the method: its first lines list everything the method refuses. Return a value for an outcome the caller expects, such as an item that is out of stock, and throw an exception for a call that should never have been made, such as a negative quantity from code you control.

Checkpoint

Check: Guard clauses

  1. One question Which message does Guarded return for an empty item and a quantity of 0, Guarded("", 0)?

    Choose one answer.

    A hint

    The guards run from the top, and the first one that applies returns.

    Show the answer: One question

    Answer: item is empty

    Guard clauses run in order and the first one that applies leaves the method. Put the guards in the order you want the problems reported, usually the most basic problem first.

A switch statement runs one section and never falls through

When one value decides between several actions, a switch statement lists them as sections. Each section has one or more case labels and ends with break, return, throw or goto. C# evaluates the value once, compares it with the labels from top to bottom, and runs the section of the first label that matches. default is taken only when no label matches, wherever it is written. This cart program sends eight commands through one switch:

commands.cs: eight commands through one switch statement C# · commands/commands.cs
// A switch statement runs the one section whose case label matches. Each section ends with break, return, throw
// or goto, so control never falls through into the next section.
string[] commands = ["add", "ADD", "list", "remove", "rm", "rm", "help", "quit"];
int items = 0;
foreach (string command in commands)
{
    switch (command.ToLowerInvariant())
    {
        case "add":
            items++;
            Console.WriteLine($"{command}: added, {items} in the cart");
            break;
        case "remove" when items == 0:
        case "rm" when items == 0:
            Console.WriteLine($"{command}: the cart is already empty");
            break;
        case "remove":
        case "rm":
            items--;
            Console.WriteLine($"{command}: removed, {items} in the cart");
            break;
        default:
            Console.WriteLine($"{command}: unknown command");
            break;
        case "help":
            Console.WriteLine($"{command}: the commands are add, remove (or rm) and list");
            goto case "list";
        case "list":
            Console.WriteLine($"{command}: {items} in the cart");
            break;
    }
}

Output

add: added, 1 in the cart
ADD: added, 2 in the cart
list: 2 in the cart
remove: removed, 1 in the cart
rm: removed, 0 in the cart
rm: the cart is already empty
help: the commands are add, remove (or rm) and list
help: 0 in the cart
quit: unknown command

Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run commands.cs

ADD matched case "add": because the switch looks at command.ToLowerInvariant(). Two labels can share a section, as remove and rm do, and a when clause after a label adds a condition to it, a case guard: A when clause after a case pattern, as in case "rm" when items == 0: the label or arm is chosen only when the pattern matches and the condition is true.. The second rm found the cart empty, so the guarded label above matched first and nothing was removed. Order matters here as it did for if: written below the unguarded labels, the guarded ones could never be reached, and the compiler would stop with an error. Step through how help finds its section:

Interactive · stage by stage How the switch statement finds the section for help Step through the labels from the top. The arrow aside is the jump that goto case makes.
How the switch statement finds the section for help, step 7 of 7. Showing The value: "help", case "add": (no match), remove and rm labels (no match), default: (skipped for now), case "help": (match), case "list":, After the switch. In focus: After the switch.goto case "list"The value: "help"lower-cased oncecase "add":no matchremove and rm labelsno matchdefault:skipped for nowcase "help":matchcase "list":prints the cartAfter the switchthe next commandbreak

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. One value. The switch evaluates command.ToLowerInvariant() once. For the command help that gives "help", and every label is compared with that one value.
  2. Not add. "help" is not "add", so the add section is skipped and the next label is compared.
  3. Not remove or rm. None of the four remove and rm labels matches. A guarded label matches only when both its constant and its when condition do, so the guards make no difference here.
  4. default waits. default stands in the middle of the switch, but it is taken only when no other label matches, wherever it is written. The comparison goes on.
  5. help matches. case "help": matches, so its section runs and prints the line that lists the commands.
  6. goto case. The section ends with goto case "list"; instead of break, so control jumps into the list section on purpose. It is the only way one section continues into another.
  7. break leaves. The list section prints "help: 0 in the cart" and ends with break, which leaves the switch. The loop moves on to the last command, quit, which only default matches.

goto case "list"; is the one way to continue into another section, and it says so in the code. What C# never does is fall-through: Letting the statements of one switch section run on into the next section. C# forbids it with error CS0163; goto case jumps to another section on purpose.: let a section without an ending run on into the next one. This program leaves out the break of its add section, and the build stops:

fallthrough/Program.cs C# · fallthrough/Program.cs
// The add section has no break, so it would run on into the list section. C# refuses to build it.
string command = args.Length > 0 ? args[0] : "add";
int items = 0;
switch (command)
{
    case "add":
        items++;
        Console.WriteLine("added");
    case "list":
        Console.WriteLine($"{items} in the cart");
        break;
}
Building fallthrough Python · fallthrough_session.py
from session import run

# fallthrough leaves out the break of its add section. It is built once.
run("""
cd fallthrough
!dotnet build -v q
""")

Output

$ cd fallthrough
$ dotnet build -v q
fallthrough/Program.cs(6,5): error CS0163: Control cannot fall through from one case label ('case "add":') to another [fallthrough/App.csproj]

Build FAILED.

fallthrough/Program.cs(6,5): error CS0163: Control cannot fall through from one case label ('case "add":') to another [fallthrough/App.csproj]
    0 Warning(s)
    1 Error(s)

Time Elapsed …
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 fallthrough_session.py

Error CS0163 names the label whose section has no ending, case "add":. Add break; to stop there, or goto case "list"; when running the list section next is really what you want.

Checkpoint

Check: Switch statements

  1. Question 1 of 2 If the first command sent to commands.cs were REMOVE, with an empty cart, what would it print?

    Choose one answer.

    A hint

    The switch looks at the command in lower case, and the guarded labels come first.

    Show the answer: Question 1 of 2

    Answer: REMOVE: the cart is already empty

    A case guard adds a condition to its label: case "remove" when items == 0 matches only an empty cart. Because the labels are compared from the top, the guarded label must come before the unguarded one.

  2. Question 2 of 2 A switch section ends without break, return, throw or goto, and another section follows it. What happens?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The build fails with error CS0163

    Every switch section must end with break, return, throw or goto. To run another section next on purpose, end the section with goto case and that label, as the help section of commands.cs does.

A switch expression maps one value to one result

A switch statement chooses what to do; a switch expression: An expression such as income switch { <= 250_000 => 0, _ => 30 }: it tests its arms in the order written and evaluates to the result of the first arm whose pattern matches. chooses a value. The input comes first, then switch and a list of arms in braces, separated by commas. Each arm is a pattern, an optional when guard, => and the result. The arms are tested in the order they are written, and the first arm whose pattern matches gives the value of the whole expression. The five bands become five arms, plus one for an income that is not valid at all:

bands_switch.cs: the same bands as a switch expression C# · bands-switch/bands_switch.cs
// The same made-up bands as a switch expression with relational patterns, compared with the if version.
int[] incomes = [180_000, 250_000, 250_001, 950_000, 1_100_000, 2_400_000];
foreach (int income in incomes)
{
    Console.WriteLine($"{income,9:N0}: {Rate(income),2} %");
}

int compared = 0, differ = 0;
for (int income = 0; income <= 3_000_000; income += 500)
{
    compared++;
    if (Rate(income) != RateIf(income))
    {
        differ++;
    }
}
Console.WriteLine($"{compared:N0} incomes from 0 to 3,000,000 compared with the if version: {differ} differ");

try
{
    Rate(-5);
}
catch (ArgumentOutOfRangeException e)
{
    Console.WriteLine($"Rate(-5) threw {e.GetType().Name}");
}

static int Rate(int income) => income switch
{
    < 0 => throw new ArgumentOutOfRangeException(nameof(income), income, "An income cannot be negative."),
    <= 250_000 => 0,
    <= 600_000 => 5,
    <= 1_100_000 => 10,
    <= 2_000_000 => 20,
    _ => 30,
};

static int RateIf(int income)
{
    if (income <= 250_000)
    {
        return 0;
    }
    if (income <= 600_000)
    {
        return 5;
    }
    if (income <= 1_100_000)
    {
        return 10;
    }
    if (income <= 2_000_000)
    {
        return 20;
    }
    return 30;
}

Output

  180,000:  0 %
  250,000:  0 %
  250,001:  5 %
  950,000: 10 %
1,100,000: 10 %
2,400,000: 30 %
6,001 incomes from 0 to 3,000,000 compared with the if version: 0 differ
Rate(-5) threw ArgumentOutOfRangeException

Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run bands_switch.cs

< 0 and <= 250_000 are relational pattern: A pattern that compares the input with a constant using <, >, <= or >=, such as <= 250_000. Relational patterns were added in C# 9.: they compare the input with a constant. Each arm’s pattern only has to state the top of its band, because an income below it was taken by an arm above, exactly as in the if chain. The last arm, _, is the discard pattern: The pattern _, which matches any value. As the last arm of a switch expression it catches every input the other arms left., which matches any value, so every income above 2,000,000 gets 30 %. An arm’s result can be a throw expression: a negative income belongs to no band, and Rate(-5) threw ArgumentOutOfRangeException.

The program then compared the switch expression with an if version on 6,001 incomes, every 500 from 0 to 3,000,000, and they differed on none. The switch expression is shorter, it is one expression, so it can be the whole body of a method after =>, and the compiler can check it in a way it cannot check the chain, as the next section shows.

Key idea

A switch statement chooses between actions and a switch expression chooses a value. Both test their cases from the top and take the first that matches, so the order of the cases carries meaning.

Checkpoint

Check: Switch expressions

  1. Question 1 of 2 What does Rate(600_001) return, with the arms of bands_switch.cs?

    Type a number.

    A hint

    Test the arms from the top: < 0, <= 250_000, <= 600_000, <= 1_100_000 …

    Show the answer: Question 1 of 2

    Answer: 10 %

    600,001 is not below 0, not at most 250,000 and not at most 600,000, but it is at most 1,100,000, so the fourth arm gives 10.

  2. Question 2 of 2 In bands_switch.cs, which incomes does the last arm, _ => 30, receive?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Every income that no arm above it matched: all incomes above 2,000,000

    The discard pattern matches everything, so as the last arm it catches every value that the other arms left and makes the switch expression exhaustive.

A switch expression that misses a value warns at build time and throws at run time

When no arm matches, a switch expression has no value to give, so the runtime throws System.Runtime.CompilerServices.SwitchExpressionException. The compiler warns you first whenever it can find an input that no arm covers. The first program below lost the arm for the top band; the second names every member of an enum:

missing-arm/Program.cs C# · missing-arm/Program.cs
// The bands as a switch expression that forgot the top band, above 2,000,000.
int[] incomes = [950_000, 2_400_000];
foreach (int income in incomes)
{
    Console.WriteLine($"{income}: {RateFor(income)} %");
}

static int RateFor(int income) => income switch
{
    <= 250_000 => 0,
    <= 600_000 => 5,
    <= 1_100_000 => 10,
    <= 2_000_000 => 20,
};
status/Program.cs C# · status/Program.cs
// The switch expression names every member of Status, yet a Status variable can hold other values too.
Console.WriteLine(Label(Status.Paid));
Console.WriteLine(Label((Status)7));

static string Label(Status status) => status switch
{
    Status.New => "new",
    Status.Paid => "paid",
    Status.Shipped => "shipped",
};

enum Status
{
    New,
    Paid,
    Shipped,
}
Building and running missing-arm and status Python · missing_session.py
from session import run

# missing-arm is a switch expression without the top band, and status names every member of an enum. Each is built,
# then run.
run("""
cd missing-arm
dotnet build -v q
!dotnet run --no-build
cd ../status
dotnet build -v q
!dotnet run --no-build
""")

Output

$ cd missing-arm
$ dotnet build -v q
missing-arm/Program.cs(8,42): warning CS8509: The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern '2000001' is not covered. [missing-arm/App.csproj]

Build succeeded.

missing-arm/Program.cs(8,42): warning CS8509: The switch expression does not handle all possible values of its input type (it is not exhaustive). For example, the pattern '2000001' is not covered. [missing-arm/App.csproj]
    1 Warning(s)
    0 Error(s)

Time Elapsed …
$ dotnet run --no-build
950000: 10 %
Unhandled exception. System.Runtime.CompilerServices.SwitchExpressionException: Non-exhaustive switch expression failed to match its input.
Unmatched value was 2400000.
   at <PrivateImplementationDetails>.ThrowSwitchExpressionException(Object unmatchedValue)
   at Program.<<Main>$>g__RateFor|0_0(Int32 income) in missing-arm/Program.cs:line 8
   at Program.<Main>$(String[] args) in missing-arm/Program.cs:line 5
[exit status 134]
$ cd ../status
$ dotnet build -v q
status/Program.cs(5,46): warning CS8524: The switch expression does not handle some values of its input type (it is not exhaustive) involving an unnamed enum value. For example, the pattern '(Status)3' is not covered. [status/App.csproj]

Build succeeded.

status/Program.cs(5,46): warning CS8524: The switch expression does not handle some values of its input type (it is not exhaustive) involving an unnamed enum value. For example, the pattern '(Status)3' is not covered. [status/App.csproj]
    1 Warning(s)
    0 Error(s)

Time Elapsed …
$ dotnet run --no-build
paid
Unhandled exception. System.Runtime.CompilerServices.SwitchExpressionException: Non-exhaustive switch expression failed to match its input.
Unmatched value was 7.
   at <PrivateImplementationDetails>.ThrowSwitchExpressionException(Object unmatchedValue)
   at Program.<<Main>$>g__Label|0_0(Status status) in status/Program.cs:line 5
   at Program.<Main>$(String[] args) in status/Program.cs:line 3
[exit status 134]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 missing_session.py

Warning CS8509 even names a value the arms miss, 2000001. A warning does not stop a build, so the program ran, printed the rate for 950,000 and then threw for 2,400,000, with the unmatched value in the message. The enum version shows a quieter gap. Its arms name all three members of Status, yet the compiler warns with CS8524, because an enum variable can hold any value of its underlying int, and (Status)7 threw at run time.

Close the gap with a final arm that covers everything left, as bands_switch.cs did with _, and decide what that arm should do: give a sensible value, or throw an exception that names the bad input. Above all, read these warnings as bugs: each one is a value that would crash a running program.

Shipping a build with CS8509

A switch expression with warning CS8509 builds and passes every test that only feeds it values it covers, then throws SwitchExpressionException in production on the first input it misses. Fix the warning where it appears: add the missing arms or a final _ arm.

Checkpoint

Check: Missing cases, warnings and exceptions

  1. Question 1 of 2 missing-arm built with warning CS8509. What did the program do when it ran on 950,000 and then 2,400,000?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It printed the rate for 950,000, then threw SwitchExpressionException for 2,400,000

    The compiler warns when it can find an input that no arm covers; here it even named one, 2000001. The program still builds, so the gap shows up only at run time, as a SwitchExpressionException for the first such input.

  2. Question 2 of 2 A switch expression over enum Size { S, M, L } has one arm for each of Size.S, Size.M and Size.L. Why does the compiler still warn?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A Size variable can hold any int, such as (Size)3, and no arm matches it

    Naming every member of an enum still leaves the other values of its underlying type. A final arm with the discard pattern covers them; decide whether it should return a value or throw.

if, a switch statement or a switch expression: choosing by what the code decides

All three choose one path, so choose by what the decision looks like:

Write When In this lesson
if and else if the conditions test different values, or are not one value compared with constants the order checks of Guarded
a switch statement one value picks one of several actions, each with its own statements the cart commands
a switch expression one value maps to one result the rate of each band

A rule of thumb

If every branch ends by assigning the same variable or returning a value, the code is a mapping: write it as a switch expression. Keep a switch statement for branches that do different work.

If you know Java, two differences matter. Java’s older colon form, case L:, falls through to the next label when a break is missing, while C# refuses to build such a section. Java’s arrow form, case L ->, never falls through and is close to a C# switch expression; but a Java switch expression that is not exhaustive does not compile, while C# builds it with a warning and throws at run time.

Checkpoint

Check: Choosing if, a statement or an expression

  1. One question Each branch of an if and else if chain on deliveryZone only assigns a different number to fee. Which form fits best?

    Choose one answer.

    Show the answer: One question

    Answer: A switch expression: fee = deliveryZone switch { … }

    If every branch assigns the same variable or returns a value, the code is a mapping, and a switch expression states it in one place. Keep switch statements for branches that do different work.

Practise: map class codes to fare multipliers

fares.cs checks a method that you write. Its comments give five class codes with made-up multipliers, and nine checks call your method with known codes, codes in lower case or with spaces around them, an unknown code and null. The method you start from only throws, which fails all nine checks:

fares.cs: nine checks of the Multiplier method C# · fares/fares.cs
// Practice: give Multiplier a switch expression as its body, then run this file until all nine checks pass.
// Five class codes, with multipliers made up for this exercise: SL 1.0, CC 2.0, 3A 2.5, 2A 3.5 and 1A 6.0.
// A code matches whatever its case and the spaces around it; any other code, and null, throws an
// ArgumentException whose message names the code.
(string? Code, decimal? Expected)[] checks =
[
    ("SL", 1.0m), ("CC", 2.0m), ("3A", 2.5m), ("2A", 3.5m), ("1A", 6.0m),
    ("sl", 1.0m), (" 3a ", 2.5m), ("XX", null), (null, null),
];

int passed = 0;
foreach (var (code, expected) in checks)
{
    string shown = code is null ? "null" : $"\"{code}\"";
    string verdict;
    try
    {
        decimal got = Multiplier(code);
        verdict = got == expected ? "ok" : $"FAIL, returned {got} instead of {(expected is null ? "throwing" : expected)}";
    }
    catch (ArgumentException e) when (expected is null)
    {
        verdict = code is null || e.Message.Contains(code) ? "ok" : "FAIL, the message does not name the code";
    }
    catch (Exception e)
    {
        verdict = $"FAIL, {e.GetType().Name}";
    }
    if (verdict == "ok")
    {
        passed++;
    }
    Console.WriteLine($"Multiplier({shown}): {verdict}");
}
Console.WriteLine($"{passed} of {checks.Length} checks pass");
return passed == checks.Length ? 0 : 1;

static decimal Multiplier(string? code)
{
    throw new NotImplementedException("Write a switch expression here.");
}

Output (exit status 1)

Multiplier("SL"): FAIL, NotImplementedException
Multiplier("CC"): FAIL, NotImplementedException
Multiplier("3A"): FAIL, NotImplementedException
Multiplier("2A"): FAIL, NotImplementedException
Multiplier("1A"): FAIL, NotImplementedException
Multiplier("sl"): FAIL, NotImplementedException
Multiplier(" 3a "): FAIL, NotImplementedException
Multiplier("XX"): FAIL, NotImplementedException
Multiplier(null): FAIL, NotImplementedException
0 of 9 checks pass

Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run fares.cs

Write Multiplier as a switch expression

Copy fares.cs to your computer and replace the body of Multiplier with a switch expression on code?.Trim().ToUpperInvariant(). Make the last arm throw an ArgumentException whose message names the code, and run the file with dotnet run fares.cs until it prints 9 of 9 checks pass.

Then test yourself below: one question at a time, chosen from the parts of this lesson you have answered least well, with a reason under every choice.

Practice that adapts to you

Practise this lesson

12 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 12 In an if, else if, else chain, two of the conditions are true. How many blocks run?

    Choose one answer.

    Show the answer: Question 1 of 12

    Answer: One: the block of the first true condition

    An else if chain runs at most one block, the first whose condition is true; the else block runs only when none is.

  2. Question 2 of 12 Someone writes the bands with < instead of <=: if (income < 250_000) rate = 0; else if (income < 600_000) rate = 5; … What rate does an income of exactly 250,000 get?

    Choose one answer.

    A hint

    Is 250,000 < 250,000 true?

    Show the answer: Question 2 of 12

    Answer: 5 %

    < and <= differ only at the boundary, and that is exactly where an off-by-one charges the wrong rate. Decide which band each boundary belongs to, then pick the operator.

  3. Question 3 of 12 What is a guard clause?

    Choose one answer.

    Show the answer: Question 3 of 12

    Answer: A check at the start of a method that returns or throws at once when an input is bad

    Guard clauses keep the main path flat: one bad case per check, each with its own message, and the work at the end without indentation.

  4. Question 4 of 12 A method reads: if (user is not null) { if (user.IsActive) { if (cart.Count > 0) { Checkout(user, cart); } } }. Rewritten with guard clauses, what is its first statement?

    Choose one answer.

    Show the answer: Question 4 of 12

    Answer: if (user is null) return;

    Each nested condition becomes a guard with the condition inverted, in the same order: no user, an inactive user, an empty cart. After the three guards, Checkout(user, cart) runs without any indentation.

  5. Question 5 of 12 What does this print?

    Read the code, then choose one answer.

    int extras = 0;
    switch ("deluxe")
    {
        case "deluxe":
            extras += 2;
            goto case "standard";
        case "standard":
            extras += 1;
            break;
    }
    Console.WriteLine(extras);
    Show the answer: Question 5 of 12

    Answer: 3

    goto case jumps to another section on purpose, the way help jumped to list in commands.cs. It is the explicit replacement for the fall-through that C# forbids.

  6. Question 6 of 12 A switch statement has default as its first section. When does that section run?

    Choose one answer.

    Show the answer: Question 6 of 12

    Answer: Only when no other label matches

    The default section can appear anywhere in a switch statement and still runs only when no other case matches.

  7. Question 7 of 12 Which of these can be the whole body of a method written with =>, as in static int Rate(int income) => … ?

    Choose one answer.

    Show the answer: Question 7 of 12

    Answer: A switch expression

    A switch expression evaluates to the result of its first matching arm, so it can be returned, assigned or used as an expression body.

  8. Question 8 of 12 The arms of Rate are reordered so that <= 2_000_000 => 20 comes first, before <= 600_000 => 5. What happens?

    Choose one answer.

    Show the answer: Question 8 of 12

    Answer: The build stops with an error, because the arms after it can never be chosen

    Order matters in a switch expression as in an if chain, but the compiler checks a switch expression: an arm that an earlier arm makes unreachable is an error, not a silent bug.

  9. Question 9 of 12 On .NET 10, what does a switch expression throw when no arm matches its input?

    Choose one answer.

    Show the answer: Question 9 of 12

    Answer: SwitchExpressionException, from System.Runtime.CompilerServices

    When no arm matches, the runtime throws SwitchExpressionException. The compiler's CS8509 and CS8524 warnings point at the inputs that would do this before the program ever runs.

  10. Question 10 of 12 Why could missing-arm run at all, although its switch expression misses every income above 2,000,000?

    Choose one answer.

    Show the answer: Question 10 of 12

    Answer: CS8509 is a warning, and warnings do not stop a build

    The compiler warns about the gap but builds the program. Treat these warnings as bugs and fix them where they appear, before an input that no arm covers reaches a running program.

  11. Question 11 of 12 A developer coming from Java leaves out break at the end of a C# switch section on purpose, to run the next section too. What happens?

    Choose one answer.

    Show the answer: Question 11 of 12

    Answer: The build fails with CS0163; the C# way is goto case

    In C# every switch section ends with break, return, throw or goto. Several labels can share one section, and goto case jumps to another section on purpose.

  12. Question 12 of 12 For each kind of order event, a program writes a different log line, updates a different counter and may send an email. Which form fits best?

    Choose one answer.

    Show the answer: Question 12 of 12

    Answer: A switch statement, one section per event

    Choose by what the decision produces: a value, a switch expression; different actions, a switch statement; conditions on different values, an if chain.

Interview questions

Fresher · How does a C# switch statement differ from a switch expression?

A switch statement chooses which statements to run: each section has case labels and must end with break, return, throw or goto, because C# never lets one section fall through into the next. A switch expression, added in C# 8, produces a value: income switch { <= 250_000 => 0, _ => 30 } is an expression, so it can be assigned, returned or used as a method body. Both test their cases from the top and take the first that matches, and both accept patterns and when guards. The compiler warns (CS8509) when a switch expression can miss an input, and at run time such a miss throws SwitchExpressionException, while a switch statement without a matching case simply does nothing. Use the statement for actions and the expression for mappings.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Fresher to mid

    You are porting Java billing code in which a switch on the plan relies on fall-through: case PREMIUM adds a premium fee and, with no break, runs on into case STANDARD, which adds the base fee and breaks. How do you write this in C#, and what do you check before you ship it?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Fresher to mid

    A checkout method decides the delivery charge with if statements nested five deep on membership, cart total, distance and a coupon. Every change breaks a case that nobody tested. How would you restructure it, and how would you show that the new version charges exactly what the old one did?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • An if and else if chain tests its conditions from the top and runs only the first block whose condition is true, so each test may rely on every test above it having failed.
  • Guard clauses test for bad input at the top of a method and return or throw at once, which keeps the main path unindented and each message next to its test.
  • A switch statement runs the section of the first matching label, every section ends with break, return, throw or goto, and falling through is a compile-time error, CS0163.
  • A switch expression maps a value to a result with arms tested in order; relational patterns compare with constants, and _ matches everything left.
  • A switch expression that can miss a value builds with warning CS8509 or CS8524 and throws SwitchExpressionException when the missing value arrives.

Terms in this lesson

case guard
A when clause after a case pattern, as in case "rm" when items == 0: the label or arm is chosen only when the pattern matches and the condition is true.
discard pattern
The pattern _, which matches any value. As the last arm of a switch expression it catches every input the other arms left.
fall-through
Letting the statements of one switch section run on into the next section. C# forbids it with error CS0163; goto case jumps to another section on purpose.
guard clause
A check at the start of a method that returns or throws at once when an input is bad, so the code after it can assume the input is good and stays unindented.
relational pattern
A pattern that compares the input with a constant using <, >, <= or >=, such as <= 250_000. Relational patterns were added in C# 9.
switch expression
An expression such as income switch { <= 250_000 => 0, _ => 30 }: it tests its arms in the order written and evaluates to the result of the first arm whose pattern matches.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.