Control Flow

Reviewed & published by Brayan K

By the end of this lesson you'll be able to make your programs decide — branching with if/else, picking from many options with switch, and matching ranges and types with pattern matching. This is what turns a fixed list of instructions into a program that reacts.

Part of the free C# course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

💡 Real-World Analogy

Control flow is like a railway switching station. A train (your program) arrives at a junction and the switch operator (your condition) sends it down one of several tracks. An if-else is a simple fork: left or right. A switch is a complex junction with many possible tracks. Pattern matching is the automated system that reads the cargo's type and weight to route it to exactly the right platform. Without control flow, the train can only ever roll straight ahead.

📊 Control Flow Quick Reference

StructureUse WhenExample
if / else2-3 conditions, boolean logicAge check, validation
ternary ? :Simple true/false choicecond ? "A" : "B"
switch statementMany discrete valuesDay of week, menu options
switch expressionReturn a value from patternsPrice tiers, classifications
pattern matchingType checks, range checksObject classification

1. If, Else-If, and Else

The if statement runs a block of code only when its condition is true. Add else if for more checks and a final else as a catch-all. The key rule: the chain is read top to bottom and the first true branch wins — the rest are skipped. For a simple two-way choice, the ternary operator (condition ? a : b) does it in one line. Read this worked example and run it first.

using System;

class Program
{
    static void Main()
    {
        int score = 85;

        // An if-else if-else chain is checked TOP to BOTTOM.
        // The FIRST condition that is true wins; the rest are skipped.
        if (score >= 90)
        {
            Console.WriteLine("Grade: A — Excellent!");
        }
        else if (score >= 80)
        {
            Console.WriteLine("Grade: B — Great job!");   // score is 85 -> THIS runs
        }
        else if (score >= 70)
        {
            Console.WriteLine("Grade: C — Good effort");
        }
        else
        {
            Console.WriteLine("Grade: F — Please retake");
        }

        // Ternary operator: condition ? valueIfTrue : valueIfFalse
        string status = score >= 60 ? "PASS" : "FAIL";   // 85 >= 60 -> "PASS"
        Console.WriteLine($"Score: {score} — {status}");  // Score: 85 — PASS

        // Combine conditions with && (AND) and || (OR).
        int age = 20;
        bool hasID = true;
        if (age >= 18 && hasID)                           // true && true -> true
        {
            Console.WriteLine("Entry allowed");           // this line runs
        }
        else
        {
            Console.WriteLine("Entry denied");
        }
    }
}

// ✅ Expected output:
//    Grade: B — Great job!
//    Score: 85 — PASS
//    Entry allowed

Your turn. The classifier below is almost complete — fill in the four blanks marked ___ using the hints. Remember branches are checked top to bottom, so the conditions must go from smallest range upward.

using System;

class Program
{
    static void Main()
    {
        // 🎯 YOUR TURN — finish the age-bracket classifier.
        // Replace each ___ then press "Try it Yourself".

        int age = 40;

        // Branches run TOP to BOTTOM, so order from SMALLEST upward.
        if (age ___ 13)                 // 👉 use  <  so under-13s match first
        {
            Console.WriteLine("Child");
        }
        else if (age < ___)             // 👉 use 20: anyone 13..19 is a teen
        {
            Console.WriteLine("Teenager");
        }
        else if (age < 65)
        {
            Console.WriteLine("Adult"); // age is 40 -> THIS branch runs
        }
        else
        {
            Console.WriteLine("___");   // 👉 the catch-all label for 65+, e.g. Senior
        }

        // ✅ Expected output:  Adult
    }
}

2. Switch Statements & Expressions

Reach for switch when one value can take many specific forms. The traditional switch statement lists each case and needs a break to end it. The modern C# 8+ switch expression is tidier — it returns a value with the => arrow, needs no break, and supports relational patterns like >= 3 and <= 5. Use _ (the discard) as the catch-all.

using System;

class Program
{
    static void Main()
    {
        // Traditional switch STATEMENT — every case needs a break.
        int day = 3;
        switch (day)
        {
            case 1: Console.WriteLine("Monday"); break;
            case 2: Console.WriteLine("Tuesday"); break;
            case 3: Console.WriteLine("Wednesday"); break;   // day is 3 -> this prints
            case 4: Console.WriteLine("Thursday"); break;
            case 5: Console.WriteLine("Friday"); break;
            case 6:                                          // 6 and 7 share one block
            case 7: Console.WriteLine("Weekend!"); break;
            default: Console.WriteLine("Invalid day"); break;
        }

        // Modern switch EXPRESSION (C# 8+) — returns a value, no break needed.
        // 'and' / 'or' combine patterns; '_' is the catch-all (discard).
        string season = day switch
        {
            >= 3 and <= 5  => "Spring",   // day is 3 -> matches -> "Spring"
            >= 6 and <= 8  => "Summer",
            >= 9 and <= 11 => "Autumn",
            12 or 1 or 2   => "Winter",
            _              => "Invalid month"
        };
        Console.WriteLine($"Month {day}: {season}");        // Month 3: Spring
    }
}

// ✅ Expected output:
//    Wednesday
//    Month 3: Spring

Now you try. Complete the traffic-light switch expression below by filling in the two blanks — one quoted action and the discard pattern that catches anything unexpected.

using System;

class Program
{
    static void Main()
    {
        // 🎯 YOUR TURN — map a traffic-light colour to a driver action.
        // Fill in the blanks in this switch EXPRESSION.

        string light = "amber";

        string action = light switch
        {
            "red"   => "___",            // 👉 text in quotes, e.g. Stop
            "amber" => "Slow down",      // light is "amber" -> this arm is chosen
            "green" => "Go",
            ___     => "Unknown signal"  // 👉 the discard pattern: a single _
        };

        Console.WriteLine($"{light} -> {action}");

        // ✅ Expected output:  amber -> Slow down
    }
}

3. Pattern Matching

Pattern matching is one of C#'s most powerful features. A switch expression can match on value ranges (relational patterns like < 5), on types (type patterns like obj is string s), and you can attach an extra when guard for fine-grained checks. It replaces tangled if-else chains with clean, declarative code that the compiler can even check for completeness.

using System;

class Program
{
    // A switch expression as a method body — maps an age to a ticket price.
    static string GetTicketPrice(int age) => age switch
    {
        < 5            => "Free",         // ages 0-4
        >= 5 and < 13  => "£5 (Child)",   // ages 5-12
        >= 13 and < 18 => "£8 (Teen)",    // ages 13-17
        >= 18 and < 65 => "£12 (Adult)",  // ages 18-64
        >= 65          => "£6 (Senior)",  // ages 65+
        _              => "Unknown"       // negative ages, etc.
    };

    // Type patterns: branch on the runtime TYPE of a value.
    static string Classify(object obj) => obj switch
    {
        int n when n > 0 => $"Positive integer: {n}",   // type + extra 'when' guard
        int n            => $"Non-positive integer: {n}",
        string s         => $"String of length {s.Length}",
        null             => "It's null!",
        _                => $"Unknown type: {obj.GetType().Name}"
    };

    static void Main()
    {
        Console.WriteLine($"Age 4:  {GetTicketPrice(4)}");   // Age 4:  Free
        Console.WriteLine($"Age 10: {GetTicketPrice(10)}");  // Age 10: £5 (Child)
        Console.WriteLine($"Age 30: {GetTicketPrice(30)}");  // Age 30: £12 (Adult)
        Console.WriteLine($"Age 70: {GetTicketPrice(70)}");  // Age 70: £6 (Senior)

        Console.WriteLine(Classify(42));        // Positive integer: 42
        Console.WriteLine(Classify(-5));        // Non-positive integer: -5
        Console.WriteLine(Classify("Hello"));   // String of length 5
        Console.WriteLine(Classify(null));      // It's null!
    }
}

Putting It Together: a Cinema Entry Desk

Here's a small but real program that uses everything from this lesson at once — an if/else if chain to choose an age band, a switch expression to map that band to a price, a ternary for the VIP surcharge, and || to decide admission. Read it line by line; you understand every part now.

using System.Globalization;
using System;

class Program
{
    static void Main()
    {
        // Currency formatting follows the thread's culture: {value:C} prints
        // £ in London, $ in Boston and € in Paris. Pin it when the output has
        // to be the same everywhere — as it does on a page that shows you the
        // result.
        CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("en-GB");

        // === Cinema entry desk — combines this whole lesson ===
        int age = 16;
        string ticketType = "standard";   // "standard" or "vip"
        bool hasGuardian = true;

        // 1) if / else-if / else picks the age band (first true wins, top-down).
        string band;
        if (age < 13)            band = "Child";    // not this one
        else if (age < 18)       band = "Teen";     // age 16 -> THIS one
        else if (age < 65)       band = "Adult";
        else                     band = "Senior";

        // 2) switch EXPRESSION maps the band to a base price (no break needed).
        decimal basePrice = band switch
        {
            "Child"  => 5.00m,
            "Teen"   => 8.00m,   // band is "Teen" -> 8.00
            "Adult"  => 12.00m,
            "Senior" => 6.00m,
            _        => 0m       // catch-all so it can never throw
        };

        // 3) Ternary + && add a VIP surcharge only for vip tickets.
        decimal price = ticketType == "vip" ? basePrice + 5.00m : basePrice; // 8.00

        // 4) An 18-rated film needs an adult OR a guardian for under-18s.
        bool admitted = age >= 18 || hasGuardian;   // 16>=18 false, but guardian true

        Console.WriteLine($"Band:     {band}");        // Band:     Teen
        Console.WriteLine($"Price:    {price:C}");      // Price:    £8.00
        Console.WriteLine($"Admitted: {admitted}");    // Admitted: True
    }
}

// ✅ Expected output:
//    Band:     Teen
//    Price:    £8.00
//    Admitted: True

This is the shape of nearly all real logic: a few branches pick a category, a mapping turns that category into a value, and a final condition decides the outcome.

Common Errors (and the fix)

Pro Tips

Frequently Asked Questions

Q: When should I use a switch instead of if/else if?

Use switch when you're checking one value against many specific options (a day number, a status string, an age band). Use if/else if when conditions are unrelated or combine several variables with && and ||.

Q: What's the difference between a switch statement and a switch expression?

A statement does something in each case (and needs break). An expression returns a value using the => arrow and is assigned to a variable: string s = day switch { ... };. The expression form is the modern default for mapping input to output.

Q: Do I always need an else or a _ case?

Not for an if — it can run nothing if no branch matches. But a switch expression should always have a _ arm, otherwise it throws SwitchExpressionException when nothing matches.

Q: Why did my code assign instead of compare?

You wrote = (assignment) where you meant == (equality). In C# a condition must be a bool, so if (x = 5) is a compile error — a helpful reminder to use ==.

📋 Quick Reference

TaskCodeNotes
Branchif (x > 0) { ... } else { ... }First true block runs
One-line choicevar s = ok ? "Y" : "N";Ternary operator
Multi-caseswitch (d) { case 1: ...; break; }Needs break
Return a valuevar n = d switch { 1 => "Mon", _ => "?" };No break; needs _
Range pattern>= 18 and < 65 => "Adult"Relational + and/or
Type patternobj is string sTest type + cast

Mini-Challenge: FizzBuzz

No blanks this time — just a brief and an outline. FizzBuzz is the classic control-flow exercise (and a real interview question): loop 1 to 15, and for each number print "Fizz", "Buzz", "FizzBuzz", or the number itself. The trick is checking the "both" case first. Build it, run it, and match the expected output in the comments.

using System;

class Program
{
    static void Main()
    {
        // 🎯 MINI-CHALLENGE: FizzBuzz for 1..15
        // 1. Loop the numbers 1 to 15 with:  for (int i = 1; i <= 15; i++)
        // 2. For each number, use if / else if / else to decide what to print:
        //      - divisible by BOTH 3 and 5  -> "FizzBuzz"   (test:  i % 15 == 0)
        //      - divisible by 3 only        -> "Fizz"       (test:  i % 3 == 0)
        //      - divisible by 5 only        -> "Buzz"       (test:  i % 5 == 0)
        //      - otherwise                  -> the number itself
        // 3. Print one result per line.
        // TIP: check "both" FIRST, or 15 would wrongly print "Fizz".
        //
        // ✅ Expected output:
        //    1
        //    2
        //    Fizz
        //    4
        //    Buzz
        //    Fizz
        //    7
        //    8
        //    Fizz
        //    Buzz
        //    11
        //    Fizz
        //    13
        //    14
        //    FizzBuzz

        // your code here
    }
}

🎉 Lesson Complete

Practice quiz

In an if / else if / else chain, which branch runs?

  • All matching branches
  • The last matching branch
  • The first branch whose condition is true
  • A random branch

Answer: The first branch whose condition is true. The chain is checked top to bottom and the FIRST true branch wins; the rest are skipped.

In a traditional switch STATEMENT, what must end each non-empty case?

  • break (or return)
  • A semicolon only
  • continue
  • Nothing

Answer: break (or return). C# does not allow silent fall-through, so each non-empty case needs break or return.

What does a switch EXPRESSION use as its catch-all (default) pattern?

  • default:
  • else
  • *
  • _

Answer: _. The discard pattern _ matches anything in a switch expression, acting as the catch-all.

Which arrow does a switch expression use to map a pattern to a result?

  • ->
  • =>
  • :
  • ==

Answer: =>. Switch expressions use the => arrow, e.g. day switch { 1 => "Mon", _ => "?" }.

What does the relational pattern >= 3 and <= 5 match?

  • Values from 3 to 5 inclusive
  • Exactly 3 or 5
  • Any value below 3
  • Only 4

Answer: Values from 3 to 5 inclusive. It combines two relational patterns with 'and', matching values in the range 3 to 5 inclusive.

What does the type pattern obj is string s do?

  • Always throws
  • Converts obj to a string
  • Tests if obj is a string AND casts it to s in one step
  • Compares obj to "string"

Answer: Tests if obj is a string AND casts it to s in one step. It checks the runtime type and, if it matches, assigns the cast value to s — test and cast in one step.

What happens if a switch expression has no matching arm and no _ ?

  • It returns null
  • It throws a SwitchExpressionException at runtime
  • It returns 0
  • It skips silently

Answer: It throws a SwitchExpressionException at runtime. Without a matching pattern or a _ catch-all, a switch expression throws SwitchExpressionException.

Why must if (age < 13) come before if (age < 65) in a top-down chain?

  • Alphabetical order
  • It does not matter
  • To avoid a syntax error
  • Otherwise the broader condition matches first and the narrower one becomes unreachable

Answer: Otherwise the broader condition matches first and the narrower one becomes unreachable. Branches are checked top to bottom; ordering from smallest range upward keeps each branch reachable.

What is the ternary operator best used for?

  • Looping
  • Choosing a value in one short expression
  • Defining a method
  • Declaring a class

Answer: Choosing a value in one short expression. The ternary condition ? a : b is a one-line if/else that produces a value.

When is a switch generally preferred over a long if/else if chain?

  • Never
  • When combining unrelated variables
  • When checking one value against many specific options
  • Only inside loops

Answer: When checking one value against many specific options. switch shines when one value is matched against many discrete options, like a day number or status string.

Continue this course