Last updated: December 2025

C# Control Flow: if/else, switch, Loops & Jump Statements

Complete Guide — 2025 Edition · Includes C# 8/9/10 switch expressions & pattern matching

By CoodeVerse Editorial Team ✓ 2025 Verified ⏱ 18 min read 🎯 Beginner–Intermediate 📦 C# 8/9/10/11
Difficulty:
Beginner — Prerequisites: Variables & Types

⚡ Quick Answer: Control Flow in C#

Control flow is the foundation of any program — it determines which code runs, when, and how many times. This guide covers every C# control flow construct, including modern features like switch expressions and pattern matching that beginners often miss.

🔀

if / else

Conditions & branching

🎛️

switch

Statement & expression

🔍

Pattern Matching

C# 8/9/10 features

🔁

Loops

for / while / foreach

⏭️

Jump Statements

break / continue / goto

Modern C# Flow

??, ?., ternary, guards

📋

Best Practices

Common mistakes & fixes

FAQ

Interview questions

🔀 Section 1

if / else / else if — Conditions and Branching

The if statement evaluates a bool expression and executes its body only when it is true. Unlike C/C++, C# requires a strict bool — you cannot write if (x = 5) or if (1); the compiler rejects non-boolean expressions.

if_else_demo.csC#
using System;

int score = 78;

// Standard if / else if / else chain
if (score >= 90)
{
    Console.WriteLine("Grade: A");
}
else if (score >= 70)
{
    Console.WriteLine("Grade: B");   // ← this runs for score=78
}
else if (score >= 50)
{
    Console.WriteLine("Grade: C");
}
else
{
    Console.WriteLine("Grade: F");
}

// Nested if — find quadrant
int x = 3, y = -2;
if (x > 0)
{
    if (y > 0) Console.WriteLine("Quadrant I");
    else       Console.WriteLine("Quadrant IV");  // ← x>0, y<0
}
else
{
    if (y > 0) Console.WriteLine("Quadrant II");
    else       Console.WriteLine("Quadrant III");
}
Output
Grade: B
Quadrant IV
C# safety: no implicit bool conversion. if (x = 5) is a compile error in C# because the assignment returns int, not bool. This eliminates the classic C/C++ bug where == is accidentally written as =.
🎛️ Section 2

switch — Classic Statement & Modern Expression

C# switch has evolved significantly. The original statement syntax is still valid but the modern switch expression (C# 8.0+) is shorter, more expressive, and type-checked for exhaustiveness.

C# 1–7
switch statement — integers, chars, strings, enums. Requires break. Fall-through must use goto case.
C# 7
Type patternscase int i when i > 0:. First major pattern matching addition.
C# 8
switch expressionx switch { pat => val, _ => default }. Produces a value, no break needed, _ is default arm.
C# 9
Relational & logical patterns>= 90, > 0 and < 100, not null.
C# 10+
Extended property patterns{ Address.City: "Seattle" }. Nested pattern matching.
switch_statement.csC#
string day = "Wednesday";

switch (day)
{
    case "Monday":
        Console.WriteLine("Start of work week");
        break;
    case "Friday":
        Console.WriteLine("End of work week");
        break;
    case "Saturday":
    case "Sunday":         // multiple cases share one block (empty fall-through OK)
        Console.WriteLine("Weekend!");
        break;
    default:
        Console.WriteLine("Midweek");  // ← runs for Wednesday
        break;
}

// Explicit fall-through with goto case (the ONLY legal way in C#)
int errorCode = 404;
switch (errorCode)
{
    case 404:
        Console.WriteLine("Not Found");
        goto case 0;   // explicit fall-through — clearly intentional
    case 0:
        Console.WriteLine("(logging error)");
        break;
}
Output
Midweek
Not Found
(logging error)
switch_expression.cs — C# 8+C# 8+
// Switch expression: produces a VALUE, no break needed
string day = "Monday";
string message = day switch
{
    "Monday"                   => "Start of work week",
    "Friday"                   => "End of work week",
    "Saturday" or "Sunday"    => "Weekend!",   // C# 9 'or' pattern
    _                          => "Midweek"        // _ = default arm
};
Console.WriteLine(message);  // Start of work week

// Relational patterns (C# 9) — score to grade
int score = 82;
string grade = score switch
{
    >= 90                      => "A",
    >= 70 and < 90           => "B",   // ← matches 82
    >= 50 and < 70           => "C",
    < 0                        => "Invalid",
    _                          => "F"
};
Console.WriteLine($"Score {score} → Grade {grade}");  // Grade B

// Property pattern — object property matching
var point = (3, -2);
string quadrant = point switch
{
    ( > 0, > 0) => "Quadrant I",
    ( < 0, > 0) => "Quadrant II",
    ( < 0, < 0) => "Quadrant III",
    ( > 0, < 0) => "Quadrant IV",  // ← x=3, y=-2
    _           => "On axis"
};
Console.WriteLine(quadrant);
Output
Start of work week
Score 82 → Grade B
Quadrant IV
🔍 Section 3

Pattern Matching in C# 8/9/10

Pattern matching lets you test an expression against a shape or value and extract information in a single step. It's available in switch expressions, if statements with is, and other contexts.

pattern_matching.csC# 8/9/10
using System;

// 1. Type pattern with 'is' — declare and check in one step
object obj = "Hello";
if (obj is string s)
{
    Console.WriteLine($"String of length {s.Length}");  // s is bound here
}

// 2. Null check pattern
string? name = null;
if (name is not null)   // C# 9 'not' pattern — cleaner than != null
    Console.WriteLine(name);
else
    Console.WriteLine("name is null");

// 3. Type pattern in switch — heterogeneous collection
object[] items = { 42, "hello", 3.14, true, null };
foreach (var item in items)
{
    string desc = item switch
    {
        int i   when i > 0 => $"Positive int: {i}",
        int i               => $"Non-positive int: {i}",
        string str          => $"String: {str}",
        double d            => $"Double: {d}",
        bool b              => $"Bool: {b}",
        null                => "null value",
        _                   => "unknown"
    };
    Console.WriteLine(desc);
}
Output
String of length 5
name is null
Positive int: 42
String: hello
Double: 3.14
Bool: True
null value
🔁 Section 4

All 4 Loop Types — for, while, do-while, foreach

LoopCondition checkedMin iterationsBest used when
forBefore each iteration0You know the exact count
whileBefore each iteration0Count unknown, may never run
do-whileAfter each iteration1 (always runs once)Body must run at least once (e.g., menu)
foreachAfter each element0Iterating any collection (preferred)
all_loops.cs — all 4 loop typesC#
using System;
using System.Collections.Generic;

// 1. for — count-controlled
Console.WriteLine("for:");
for (int i = 1; i <= 3; i++)
    Console.Write($"{i} ");
Console.WriteLine();

// 2. while — condition-first
Console.WriteLine("while:");
int j = 1;
while (j <= 3)
{
    Console.Write($"{j} ");
    j++;
}
Console.WriteLine();

// 3. do-while — body first, condition after
Console.WriteLine("do-while:");
int k = 1;
do
{
    Console.Write($"{k} ");
    k++;
} while (k <= 3);
Console.WriteLine();

// 4. foreach — collection iteration (preferred)
Console.WriteLine("foreach:");
var colors = new[] { "Red", "Green", "Blue" };
foreach (string color in colors)
    Console.Write($"{color} ");
Console.WriteLine();

// foreach over Dictionary (key-value pairs)
Console.WriteLine("foreach dict:");
var scores = new Dictionary<string, int> { ["Alice"]=95, ["Bob"]=82 };
foreach (var (name, score) in scores)
    Console.WriteLine($"  {name}: {score}");
Output
for: 1 2 3
while: 1 2 3
do-while: 1 2 3
foreach: Red Green Blue
foreach dict:
Alice: 95
Bob: 82
Prefer foreach over for when iterating collections. foreach is shorter, has no off-by-one risk, works with any IEnumerable<T>, and communicates intent clearly. Use for when you need the index for logic, backwards iteration, or modifying elements in place.
⏭️ Section 5

Jump Statements: break, continue, goto, return

jump_statements.csC#
using System;

// break — exit innermost loop immediately
Console.WriteLine("break:");
for (int i = 1; i <= 6; i++)
{
    if (i == 4) break;
    Console.Write($"{i} ");
}
Console.WriteLine();   // 1 2 3

// continue — skip to next iteration
Console.WriteLine("continue (odds only):");
for (int i = 1; i <= 6; i++)
{
    if (i % 2 == 0) continue;
    Console.Write($"{i} ");
}
Console.WriteLine();   // 1 3 5

// return inside foreach — cleanest way to exit nested loops
static bool ContainsNegative(int[] arr)
{
    foreach (int x in arr)
        if (x < 0) return true;   // exits method + loop
    return false;
}
Console.WriteLine(ContainsNegative(new[] { 1, -3, 5 }));  // True

// goto case — only accepted goto use in switch
int code = 2;
switch (code)
{
    case 1: Console.WriteLine("case 1"); break;
    case 2:
        Console.WriteLine("case 2 → also running case 3");
        goto case 3;   // explicit fall-through
    case 3: Console.WriteLine("case 3"); break;
}
Output
break: 1 2 3
continue (odds only): 1 3 5
True
case 2 → also running case 3
case 3
Use return instead of break for nested loops. If you need to exit multiple nested loops at once, extract the loop logic into a method and use return. This is cleaner than a bool done flag or goto and makes the intent obvious.
✨ Section 6

Modern C# Control Flow: ??, ?., Ternary & Guard Clauses

modern_flow.cs — null handling + guard clausesC# 8+
using System;

// Ternary operator — concise two-branch assignment
int score = 75;
string result = score >= 60 ? "Pass" : "Fail";
Console.WriteLine(result);  // Pass

// ?? (null coalescing) — return left if non-null, else right
string? input = null;
string name = input ?? "Anonymous";
Console.WriteLine(name);  // Anonymous

// ??= (null coalescing assignment, C# 8) — assign only if null
input ??= "Default";
Console.WriteLine(input);  // Default

// ?. (null-conditional) — short-circuit on null
string? maybeNull = null;
int? len = maybeNull?.Length;   // null, not NullReferenceException
Console.WriteLine(len ?? -1);    // -1

// Guard clauses — early return to flatten nesting
static string ProcessInput(string? s)
{
    if (s is null)          return "Error: null input";    // guard
    if (s.Length == 0)      return "Error: empty string";  // guard
    if (s.Length > 100)    return "Error: too long";      // guard

    // Happy path — unnested, clearly visible
    return s.Trim().ToUpper();
}

Console.WriteLine(ProcessInput(null));      // Error: null input
Console.WriteLine(ProcessInput("hello"));  // HELLO
Output
Pass
Anonymous
Default
-1
Error: null input
HELLO
⚖️ Section 7

C# vs C/C++ Control Flow Differences

FeatureC#C / C++
if condition type✓ Must be bool✗ Any numeric (0=false)
if (x = 5)✓ Compile error✗ Silent bug — always true
switch on strings✓ Yes✗ No (C has no string switch)
switch fall-through✓ Compile error unless goto case✗ Silent implicit fall-through
switch expressions✓ C# 8+ (concise, value-producing)✗ Not available
Pattern matching✓ C# 7+ (type, property, relational)⚠ C++17 limited via if constexpr
foreach loop✓ Built-in, works on IEnumerable⚠ C++11 range-for (begin/end)
goto restrictions✓ Same method only + goto case✗ Any label in function, error-prone
Null coalescing ??✓ Built-in operator✗ Not available
Null-conditional ?.✓ Built-in operator✗ Not available

Best Practices & Common Mistakes

✅ Do these

❌ Avoid these

FAQ / Interview Questions

Switch statement uses case labels, requires break, and cannot produce a value directly. Switch expression (C# 8+) uses => arms, produces a value that can be assigned, requires no break, and uses _ for the default arm. For simple value mappings, switch expressions are preferred — they're shorter, cleaner, and the compiler warns when cases aren't exhaustive.
Implicit fall-through is a compile error in C#. Each case must end with break, return, throw, or goto case. To explicitly fall through to another case, use goto case X; — this makes the intent clear, unlike the silent fall-through bug in C/C++. Multiple empty cases sharing one body are allowed: case "Mon": case "Tue": ... break;
while checks the condition before the first iteration — the body may never execute. do-while checks the condition after the first iteration — the body always executes at least once. Classic do-while use case: input prompts — you must show the prompt at least once regardless of initial state.
foreach uses an enumerator (IEnumerator<T>) that tracks position. Adding or removing elements changes the collection's version number, which the enumerator detects and throws InvalidOperationException to prevent undefined behavior. Fix: use a regular for loop with an index (for arrays), or collect the items to add/remove in a separate list and apply the changes after the loop.
Guard clauses are early returns at the top of a method that handle invalid/edge cases before the main logic. Instead of wrapping everything in nested if-else, you reject invalid inputs first, leaving the happy path unnested and clearly visible. They reduce nesting depth, make preconditions obvious, and are endorsed by Microsoft's C# coding guidelines. Example: if (input is null) return "Error"; if (input.Length == 0) return "Empty"; // main logic here
Pattern matching lets switch cases test by type (case int i), property value (case { Length: > 5 }), relational condition (case >= 90), or logical combination (case > 0 and < 100). Use it when: branching on the runtime type of an object (polymorphism without virtual methods), testing ranges of values, or matching complex conditions in a readable single expression. It replaces long if/else-if chains with type checks.

Related C# Topics on CoodeVerse

Variables & Types Methods & Functions Arrays & Collections LINQ Classes & Objects Exception Handling async/await 📚 Full C# Course

CoodeVerse Editorial Team

Senior .NET engineers and CS educators. All code tested with .NET 8 and C# 12.