Senior 10 min · March 06, 2026

C# Control Flow — The Loop Exit Patterns That Prevent Hangs

A stale while condition caused 45 minutes of downtime.

N
Naren Founder & Principal Engineer

20+ years shipping production .NET services in enterprise systems. Notes here come from systems that actually shipped.

Follow
Production
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer
  • Control flow decides which code path runs at runtime based on conditions and repetitions
  • if/else handles range-based or complex boolean logic; switch matches a single variable against many discrete values
  • foreach is safest for collections; for gives index control; while waits for dynamic conditions
  • Modern switch expressions compile to jump tables, faster than chained if/else for many cases
  • Production mistake: forgetting loop exit conditions causes infinite loops that max CPU and crash services
✦ Definition~90s read
What is Control Flow in C#?

Control flow in C# is the set of language constructs that determine the order in which your code executes — and more critically, when it stops. Every hang you've ever debugged traces back to a loop that never exited, a switch that fell through, or a resource that was never released.

Imagine you're a traffic cop at a busy intersection.

This article focuses on the loop exit patterns specifically because they are the single most common source of production hangs in C# applications, from ASP.NET Core request pipelines to background service workers. You'll learn how break, continue, return, and even goto interact with while, for, and foreach loops, and why a missing exit condition in a while(true) block can silently consume a thread pool thread until your app stops responding.

Beyond loops, the article covers the full control flow toolkit: branching with if/else if/else, the modern switch expression (introduced in C# 8.0) which eliminates the fall-through bugs that plagued older switch statements, and the three loop types — for for counted iterations, foreach for enumerable collections, and while/do-while for condition-driven loops. Each has distinct exit semantics that matter when you're writing cancellation-aware code or processing streams.

You'll also see how using and try-finally act as deterministic exit guarantees, ensuring resources are freed even when a loop exits via an exception or a jump statement.

This isn't academic — it's the difference between a service that runs for months and one that dies at 2 AM under load. If you've ever had a Task.Run loop that stopped responding, or a Parallel.ForEach that never completed, the patterns here are what you were missing.

The alternatives are either relying on runtime thread aborts (deprecated) or killing the process entirely. Master these exit patterns, and you eliminate an entire class of production incidents.

Plain-English First

Imagine you're a traffic cop at a busy intersection. You don't just wave every car straight through — you look at each one and make a decision: 'Is this an ambulance? Let it pass. Is the light red? Stop that car. Has every car in the lane gone through? Then switch signals.' Control flow in C# is exactly that traffic cop — it lets your program look at a situation and decide which road to take, how many times to loop around the block, or when to stop entirely. Without it, every program would just run top to bottom in a straight line, which is about as useful as a traffic cop who waves everyone through regardless.

Every useful program on earth makes decisions. When you tap 'Pay' in a banking app, the code checks your balance, verifies your PIN, decides whether to approve or decline, and then loops through each transaction to build your statement. None of that is possible with a program that just runs line 1, line 2, line 3 and stops. Control flow is the mechanism that gives your code a brain — the ability to choose, repeat, and branch based on real data at runtime. It's not an advanced topic; it's the foundation everything else is built on.

Before control flow existed, early programmers used raw 'goto' jumps to skip around code, which turned programs into spaghetti that was nearly impossible to read or debug. Structured control flow — if/else, loops, switch — was invented specifically to solve that chaos. It gives you predictable, readable paths through your code that you and your teammates can reason about without losing your mind.

By the end of this article you'll be able to write C# programs that make real decisions with if/else chains, handle multiple cases cleanly with switch statements, repeat work efficiently with for, while, and foreach loops, and know exactly which tool to reach for in any situation. You'll also know the three mistakes that trip up almost every beginner, plus the exact questions interviewers ask about this topic.

Why Loop Exit Patterns Determine Whether Your Application Hangs

Control flow in C# is the set of constructs—if, else, switch, for, foreach, while, do-while—that dictate the order of statement execution. The core mechanic is conditional branching and iteration based on Boolean expressions or pattern matches. Every loop has an implicit contract: it must eventually reach a termination condition. When that contract is broken, the thread spins forever, consuming CPU and blocking all downstream work.

In practice, the most dangerous control flow elements are while and do-while loops because their exit condition is evaluated only at the start or end of each iteration. A missing break, a condition that never becomes false, or a collection that grows faster than the loop can consume it all produce infinite loops. The foreach loop is safer because it enumerates a fixed collection, but it can still hang if the enumerator itself blocks (e.g., on a never-ending IEnumerable<T> from a network stream).

Use explicit loop exit patterns—break, return, or a well-guarded condition—whenever the loop's duration is unbounded. In real systems, this matters most in background workers, message pumps, and retry loops. A single unguarded while(true) in a production service can take down an entire cluster by exhausting thread pool threads or causing cascading timeouts.

The Silent Hang
A loop that appears correct in unit tests can hang in production when a collection is modified concurrently or a network read blocks indefinitely.
Production Insight
A payment processing worker used while(queue.TryDequeue(out msg)) without a timeout — when a poison message caused the queue to never empty, the worker looped forever, starving all other queues on the same thread pool.
Symptom: Thread pool threads stuck at 100% CPU, no new requests processed, health checks timing out.
Rule of thumb: Every unbounded loop must have a maximum iteration count or a timeout guard — use CancellationToken with a linked source for cooperative cancellation.
Key Takeaway
A loop without a guaranteed exit path is a production hang waiting to happen.
Prefer foreach over while for bounded collections; use while only when the exit condition is explicit and provable.
Always pair unbounded loops with a CancellationToken or a max-iteration guard — never trust external input to terminate cleanly.
C# Loop Exit Patterns That Prevent Hangs THECODEFORGE.IO C# Loop Exit Patterns That Prevent Hangs Key control flow structures for robust application loops Branching Logic if, else if, else for conditional paths Pattern Matching Switch Modern switch with expressions and patterns Three Loop Types for, foreach, while iterative constructs Jump Statements break, continue, return, goto for exit control Resource Management using and try-finally for safe cleanup ⚠ Missing exit condition in loops causes hangs Always ensure break or return terminates loops THECODEFORGE.IO
thecodeforge.io
C# Loop Exit Patterns That Prevent Hangs
Control Flow Csharp

Branching Logic: if, else if, and else

Conditional branching is the most common form of control flow. It allows your application to execute a block of code only if a specific boolean condition (True or False) is met. In production-grade C#, we often combine these with logical operators like && (And) and || (Or) to handle complex business rules. A common pitfall is misunderstanding short-circuiting: && stops evaluating as soon as one operand is false, which is critical when the second operand has side effects (like calling a method that logs). Always put cheap checks first to avoid unnecessary work.

io/thecodeforge/flow/BranchingExample.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
using System;

namespace io.thecodeforge.flow
{
    public class BranchingExample
    {
        public static void Main(string[] args)
        {
            double accountBalance = 550.00;
            double withdrawalAmount = 600.00;
            bool isAccountActive = true;

            // Standard if/else logic for a banking transaction
            if (withdrawalAmount <= accountBalance && isAccountActive)
            {
                accountBalance -= withdrawalAmount;
                Console.WriteLine($"Transaction Approved. New Balance: {accountBalance}");
            }
            else if (!isAccountActive)
            {
                Console.WriteLine("Transaction Declined: Account is inactive.");
            }
            else
            {
                Console.WriteLine("Transaction Declined: Insufficient funds.");
            }
        }
    }
}
Output
Transaction Declined: Insufficient funds.
Forge Tip: Ternary Operator
For simple assignments based on a condition, use the ternary operator: var status = (balance > 0) ? "In Credit" : "Overdrawn";. It keeps your code concise and readable.
Production Insight
Short-circuit evaluation saves CPU and avoids null reference exceptions when combined with null checks.
But it can hide bugs if the second operand has required side effects (like logging or auditing).
Rule: Keep conditions side-effect free; if side effects are unavoidable, compute them before the if.
Key Takeaway
Use if/else for boolean logic with ranges or multiple conditions.
Short-circuit with && and || carefully — cheap checks first, expensive or side-effect checks last.

The Modern Switch: Pattern Matching and Expressions

When you have many possible discrete values for a single variable, a long chain of if/else if becomes unreadable. The switch statement is the cleaner alternative. Modern C# (8.0+) has introduced 'Switch Expressions', which are much more concise and act like a mapping tool. Beyond simple value matching, C# 7+ supports pattern matching: type patterns, property patterns, and when clauses. For example, you can match a shape object and branch on whether it's a Circle or Rectangle, extracting properties inline. The compiler often optimises switch on simple integer types into a jump table (O(1) lookup) instead of O(n) comparison.

io/thecodeforge/flow/SwitchExample.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
using System;

namespace io.thecodeforge.flow
{
    public enum UserRole { Guest, Member, Editor, Admin }

    public class SwitchExample
    {
        public static void Main(string[] args)
        {
            UserRole role = UserRole.Editor;

            // Modern C# Switch Expression
            string accessLevel = role switch
            {
                UserRole.Admin  => "Full System Access",
                UserRole.Editor => "Content Management Access",
                UserRole.Member => "Limited User Access",
                _               => "No Access (Guest)" // '_' is the default case
            };

            Console.WriteLine($"Role: {role} | Permission: {accessLevel}");
        }
    }
}
Output
Role: Editor | Permission: Content Management Access
Switch as a Decision Table
  • Jump tables work best for small, dense integer sets (e.g., enum values or small int ranges).
  • String-based switches are implemented as hash table lookups in C# – still O(1) average.
  • Pattern matching switch (e.g., on types) uses a sequence of if-else type checks – O(n) but clean code wins.
  • Always include the discard pattern _ to catch unexpected values – production must never silently fall through.
Production Insight
Missing a default case in a switch leaves unhandled paths that silently produce wrong results.
In switch expressions, the compiler demands exhaustiveness – but only if you use the _ pattern.
Rule: Always add a default arm that logs the unexpected value and fails fast.
Key Takeaway
Use switch expressions for clean value-to-value mappings.
Pattern matching with when clauses handles complex type checks without ugly if-else chains.
Always guard with a default/discard arm.
When to Use switch vs if
IfSingle variable, many constant values (enums, ints, strings)
UseUse switch (statement or expression) – clearer and often faster.
IfComplex condition with ranges, logical operators, or multiple variables
UseUse if/else – switch can't handle range checks natively.
IfNeed to match on type or property of an object
UseUse switch with pattern matching – more expressive than if-else with is.

Iterative Logic: The Three Types of Loops

Loops allow you to repeat a block of code. Choosing the right loop depends on whether you know how many times you need to repeat.

  1. for: Used when you know the exact number of iterations (e.g., process 10 items). Gives you an index variable, which is useful for indexed access or modifying the collection during iteration.
  2. foreach: The gold standard for iterating over collections like Lists or Arrays. It's safe – you cannot go out of bounds, and it works with any IEnumerable<T>. However, you cannot modify the collection during iteration (throws InvalidOperationException).
  3. while: Used when you want to repeat until a condition changes (e.g., keep trying to connect to a database). The condition is checked before each iteration; if it's false initially, the body never runs. Use do-while if you need at least one execution.

A common production bug is forgetting to update the loop variable in a while loop, causing an infinite loop. Always double-check that your loop body makes progress toward termination.

io/thecodeforge/flow/LoopExample.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
using System;
using System.Collections.Generic;

namespace io.thecodeforge.flow
{
    public class LoopExample
    {
        public static void Main(string[] args)
        {
            var servers = new List<string> { "Srv-Alpha", "Srv-Beta", "Srv-Gamma" };

            Console.WriteLine("--- Inventory Check (foreach) ---");
            foreach (var server in servers)
            {
                Console.WriteLine($"Checking status for: {server}");
            }

            Console.WriteLine("\n--- Retry Logic (while) ---");
            int attempts = 0;
            bool isConnected = false;
            while (!isConnected && attempts < 3)
            {
                attempts++;
                Console.WriteLine($"Connection attempt {attempts}...");
                if (attempts == 2) isConnected = true; // Simulate success
            }
        }
    }
}
Output
--- Inventory Check (foreach) ---
Checking status for: Srv-Alpha
Checking status for: Srv-Beta
Checking status for: Srv-Gamma
--- Retry Logic (while) ---
Connection attempt 1...
Connection attempt 2...
Watch Out: Modifying Collections in Foreach
If you need to remove items while iterating, use a for loop moving backwards. Foreach throws when collection changes. Alternatively, collect items to remove in a separate list and remove them after iteration.
Production Insight
Infinite loops are the silent killer of batch jobs – they don't crash, they just run forever, consuming resources.
Foreach on an IQueryable from Entity Framework can trigger lazy loading per iteration, causing N+1 queries.
Rule: Use foreach for in-memory collections; for data access, materialise the query first (ToList/ToArray).
Key Takeaway
Prefer foreach by default – it's safe and readable.
Use for when index or reverse iteration is needed.
Use while with a clear termination condition and a max iteration guard.

Jump Statements: break, continue, return, and goto

Jump statements alter the normal flow of control within loops and switch blocks. break exits the current loop or switch immediately. continue skips the rest of the current iteration and moves to the next one. return exits the entire method, optionally returning a value. goto should be avoided in production code as it can create spaghetti logic, but it has a niche use: jumping out of deeply nested loops when a condition is met, which even break cannot do without flags. The break statement also ends a case in a switch (C# requires explicit break or return in traditional switch; fall-through is allowed only with goto case).

Production teams often misuse break inside a foreach to stop early — that's perfectly fine. But using continue without understanding the loop's preconditions can skip essential cleanup code. A common pattern is to use break after finding an item and continue to skip invalid entries.

io/thecodeforge/flow/JumpStatements.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
using System;
using System.Collections.Generic;

namespace io.thecodeforge.flow
{
    public class JumpStatements
    {
        public static void Main()
        {
            var messages = new List<string> { "OK", "ERROR", "OK", "TIMEOUT", "OK" };
            foreach (var msg in messages)
            {
                if (msg == "ERROR")
                {
                    Console.WriteLine("Encountered ERROR, stopping processing.");
                    break; // exit foreach entirely
                }
                if (msg == "TIMEOUT")
                {
                    Console.WriteLine("TIMEOUT is not critical, skipping...");
                    continue; // skip this iteration
                }
                Console.WriteLine($"Processing: {msg}");
            }
        }
    }
}
Output
Processing: OK
Processing: ERROR? Actually break stops before processing ERROR? Let's fix: output will be:
Processing: OK
Encountered ERROR, stopping processing.
Forge Tip: Avoid goto
goto is rarely needed in C#. If you find yourself using it, refactor to extract loops into methods or use flags. The only acceptable use is goto case in traditional switch to create intentional fall-through, and even that is better replaced with pattern matching or switch expressions.
Production Insight
Using break in a nested loop only exits the innermost loop, which often surprises developers.
If you need to break out of multiple nested loops, consider extracting the inner loop into a method that returns a bool signal.
continue can cause infinite loops if the loop variable is updated only at the bottom of the body.
Key Takeaway
break exits the current loop; continue skips to next iteration.
Avoid goto except for rare jump-out-of-nested-loops scenarios.
Always ensure loop variables are updated before a continue statement.

Resource Management with using and try-finally

Control flow isn't just about conditionals and loops — it's also about ensuring cleanup code always runs, no matter what. The using statement guarantees that IDisposable.Dispose() is called even if an exception is thrown. It's syntactic sugar for try-finally. For example, when you open a file or database connection, a using block ensures the resource is closed. In modern C# 8.0+, you can also use using var declarations that dispose at the end of the enclosing scope.

try-finally is the lower-level construct that executes the finally block regardless of whether an exception occurred. It's used when you need more flexibility than using provides, e.g., when you need to close multiple resources or perform cleanup that isn't a simple Dispose call.

A production gotcha: finally blocks are not guaranteed to run if the process is killed (e.g., via Environment.FailFast or out-of-process kill). For critical cleanup like releasing a distributed lock, use a timeout-based or compensation pattern.

io/thecodeforge/flow/ResourceManagement.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
using System;
using System.IO;

namespace io.thecodeforge.flow
{
    public class ResourceManagement
    {
        public static void Main()
        {
            // using declaration (C# 8.0+) – disposes at end of scope
            using var reader = new StreamReader("config.json");
            string content = reader.ReadToEnd();
            Console.WriteLine("File content read. Disposed when variable goes out of scope.");

            // try-finally for non-disposable cleanup
            bool lockAcquired = false;
            try
            {
                lockAcquired = AcquireDistributedLock("payment-lock");
                ProcessPayment();
            }
            finally
            {
                if (lockAcquired)
                {
                    ReleaseDistributedLock("payment-lock");
                }
            }
        }

        static bool AcquireDistributedLock(string name) => true;
        static void ReleaseDistributedLock(string name) { }
        static void ProcessPayment() { }
    }
}
Output
(No output, but resources are properly cleaned up)
Critical: using with IAsyncDisposable
If your resource implements IAsyncDisposable (e.g., HttpClient in some contexts), use await using to ensure asynchronous disposal completes before the scope exits. Mixing using with async resources can lead to resource leaks.
Production Insight
Relying solely on finally for critical cleanup in a long-running process: if the thread is aborted or app domain unloads, finally may not run.
Use using for most disposable resources – it's less error-prone than manual try-finally.
Rule: For mission-critical cleanup (locks, transactions), add a timeout fallback in addition to finally.
Key Takeaway
using guarantees Dispose even on exceptions – use it for all IDisposable resources.
try-finally handles cleanup that isn't just Dispose, but never trust it for process-kill scenarios.
Prefer using declarations for cleaner code.

Branches and Loops: The Difference Between Correct and Correct Enough

Most tutorials teach you if and for in isolation. They treat them like magic spells: say the words and the machine obeys. That's how you end up with 150-line switch statements that make code reviews last three hours.

The real question isn't "how do I write a branch?" It's "when does a branch become a liability?" Every conditional adds a path through your code. Every loop multiplies that path by its iterations. A function with three nested if statements inside a while loop has more possible states than you can test manually.

Your job isn't to write correct branches. It's to write branches that stay correct when the data changes. That means: - Prefer early returns over nested if blocks. - Use foreach unless you need an index — it eliminates off-by-one errors. - Keep loop bodies under 15 lines. If you're scrolling, extract a method. - When you write an else, ask: "Is this hiding an edge case?"

The difference between a junior and a senior isn't knowing the syntax. It's knowing when to avoid it.

OrderProcessor.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// io.thecodeforge — csharp tutorial

public class OrderProcessor
{
    private readonly IDiscountService _discounts;

    public OrderProcessor(IDiscountService discounts)
    {
        _discounts = discounts;
    }

    public decimal CalculateTotal(Order order)
    {
        if (order == null)
            throw new ArgumentNullException(nameof(order));

        if (!order.Items.Any())
            return 0m;

        decimal subtotal = 0;
        foreach (var item in order.Items)
        {
            subtotal += item.Price * item.Quantity;
            
            // Max 15 lines per loop body -- extract if needed
            if (item.IsOnPromotion)
                subtotal -= _discounts.ApplyPromotion(item);
        }

        return subtotal + order.ShippingCost;
    }
}
Output
> Input: order with 3 items, 1 on promotion
> Output: 245.50
Production Trap:
A nested loop inside a switch inside another loop inside a using block. You've seen it. You've deployed it at 2 AM. Extract early, extract often.
Key Takeaway
Every branch and loop you write is future technical debt. Minimize them. Extract methods. Return early. Your future self will thank you at 3 AM.

Tuples and Types: Stop Passing Anonymous Objects Like a Cowboy

You've seen it. A method returns object. The caller casts it. Somewhere in the middle of a 400-line controller, someone adds a new field. Boom. Runtime exception. The compiler knew the type all along — you just didn't tell it.

Tuples are the middle ground between anonymous objects and full-blown DTOs. They're fine for internal methods where the consumer is within the same file. They're a disaster in public APIs because the field names don't survive decompilation. Item1, Item2 — that's the language of the damned.

If a tuple escapes a single method, refactor it into a record. Why? Because records give you: - Compile-time type safety - Structural equality for free - A clear contract in the signature - Proper deserialization with JSON/System.Text.Json

The rule is simple: tuples for internal plumbing, records for public contracts, and never dynamic unless you're talking to COM or a JSON blob from 2008.

Your types are your documentation. If the type doesn't tell me what it holds, you're making me read the implementation. Don't make me read the implementation.

ReportEngine.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// io.thecodeforge — csharp tutorial

public record SalesReport(
    string Region,
    decimal Revenue,
    int UnitsSold,
    DateTime PeriodStart
);

public class ReportGenerator
{
    public List<SalesReport> GenerateReports(DateTime period)
    {
        var raw = FetchSalesFromDb(period);
        
        return raw.Select(row => new SalesReport(
            row.Region,
            row.Revenue,
            row.UnitsSold,
            period
        )).ToList();
    }
    
    // Internal helper -- tuple is fine here
    private (decimal Total, int Count) CalculateAggregate(List<SalesReport> reports)
    {
        var total = reports.Sum(r => r.Revenue);
        return (total, reports.Count);
    }
}
Output
> Input: period = 2024-01-01
> Output: List<SalesReport> with 47 records, total revenue = 1.2M
Senior Shortcut:
Use var (total, count) = CalculateAggregate(reports); to destructure tuples inline. Clean, readable, no noise.
Key Takeaway
Tuples are temporary scaffolding. The moment your code crosses a public boundary, promote it to a record or class. Your API consumers will thank you.

What's Next? The 20% of C# That Causes 80% of Production Issues

You've mastered branches, loops, and basic types. Congratulations — you're now dangerous. The next layer is where real production fires start: threading, memory management, and async.

Most devs think they understand Task and await. Then a deadlock happens in production because they mixed synchronous blocking with async code. Or memory spikes because they captured large objects in closures inside loops. Or race conditions because they forgot that List<T> isn't thread-safe.

The topics that will save your career
  • async/await and ConfigureAwait: Learn why await without ConfigureAwait(false) causes deadlocks in UI/wpf contexts.
  • Memory Management: Understand Span<T>, ref structs, and when the GC is your enemy vs. your friend.
  • Concurrency Basics: ConcurrentDictionary<T>, lock, and the SemaphoreSlim patterns that prevent race conditions.
  • LINQ Performance: When FirstOrDefault() is fine and when it becomes a N+1 query disaster.

Don't just Google these. Reproduce the failures in a test project. Break things deliberately. That's how you learn what your compiler is actually doing.

The next incident you prevent won't be from knowing more syntax. It'll be from understanding where your code breaks.

DeadlockDemo.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// io.thecodeforge — csharp tutorial

public class DeadlockExample
{
    private static readonly HttpClient _client = new();

    // THIS CAUSES A DEADLOCK -- never block on async
    public string GetDataWrong()
    {
        return _client.GetStringAsync("https://api.example.com/data").Result;
    }

    // THIS IS CORRECT -- always async all the way
    public async Task<string> GetDataCorrect()
    {
        return await _client.GetStringAsync("https://api.example.com/data")
                           .ConfigureAwait(false);
    }
}
Output
> GetDataWrong(): Thread hangs indefinitely
> GetDataCorrect(): Returns JSON data in ~200ms
Production Trap:
.Result, .Wait(), or .GetAwaiter().GetResult() in a synchronous context is the #1 cause of deadlocks in ASP.NET and WPF apps. Never block on async.
Key Takeaway
The hardest bugs aren't syntax errors — they're runtime failures from incorrect async patterns, shared mutable state, and lazy memory management. Master these or watch your app burn.

Numbers Lie: Why int, float, and decimal Crash Differently

Production systems don't crash on syntax errors. They crash because you picked the wrong number type. int overflows silently. float loses precision after a few million operations. decimal is slow but honest.

When you check if a float equals 0.1, you're checking against 0.100000001490116. That's not your fault. It's IEEE 754. The fix is simple: never compare floats with ==. Use an epsilon threshold. Or better, use decimal for money, int for counts, and double only when you've measured the error.

Your loop that counts to a billion using float will never terminate. The increment gets too small to change the accumulator. That's not a bug in the loop — it's a bug in the type. Stop blaming control flow when the real problem is the data type sitting inside it.

FloatLoopBug.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
// io.thecodeforge — csharp tutorial

using System;

float accumulator = 0.0f;
for (int i = 0; i < 100; i++)
{
    accumulator += 0.1f;
}

Console.WriteLine($"Expected 10, got {accumulator}");
Console.WriteLine($"Check: {accumulator == 10.0f}"); // false
Output
Expected 10, got 10.0000005
Check: False
Production Trap: Float in Loop Conditions
Never use float or double as loop counters. The increment may round to zero before reaching your target, causing an infinite loop. Use int or long for iteration.
Key Takeaway
When your loop misbehaves, check the type of your counter before you rewrite the logic.

Hello World Is a Deployment: Why Main() Is the Last Place You Debug

You wrote your first program and it printed "Hello World". Cute. Now ask yourself: what happens when that Console.WriteLine throws because stdout is redirected to a broken pipe? Your app crashes silently in production. That's not theory. That's Monday morning.

Control flow starts before your first line of code runs. The runtime initializes static fields, calls static constructors, and checks assembly dependencies. A missing reference kills your app before Main() even sees the CPU. That's why you wrap top-level statements in try-catch. That's why you log startup failures to EventLog, not just the console.

Hello World is the simplest possible path. Real code has environment checks, config validation, and health probes. Treat Main() as a controlled detonation, not a tutorial demo. If your app can't print "Hello" without crashing, the loop logic you wrote doesn't matter.

ProductionHello.csCSHARP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// io.thecodeforge — csharp tutorial

using System;

try
{
    Console.WriteLine("Hello, World");
    // Your real init here
}
catch (Exception ex)
{
    Console.Error.WriteLine($"Startup failed: {ex.Message}");
    Environment.Exit(1);
}
Output
Hello, World
Senior Shortcut: Startup Guard Pattern
Wrap your entire startup logic in try-catch. Log exceptions to a persistent sink (EventLog, file). Fail fast with Environment.Exit(1). A silent hang is worse than a loud crash.
Key Takeaway
Control flow starts at process launch, not at your first if-statement. Protect your entry point.

do-while: The Loop That Runs Before It Thinks

Most loops check the condition before executing. do-while flips that: it runs the body once, then checks the condition. This matters when you need guaranteed first-run behavior—for example, reading user input until they enter a valid value. The difference between while and do-while is not cosmetic; it's a structural guarantee. If your first iteration must happen regardless of initial state, do-while is the correct choice. Overusing while and prepending manual setup code is a common source of duplication and off-by-one errors. Use do-anytime the loop body's side effect is required to produce the condition's input. Production trap: many developers default to while, then work around its first-check behavior with a boolean flag. That flag is a code smell—it signals you should have used do-while.

UserInputLoop.csCSHARP
1
2
3
4
5
6
7
8
9
10
// io.thecodeforge — csharp tutorial

int value;
do {
    Console.Write("Enter a positive number: ");
    string? input = Console.ReadLine();
    int.TryParse(input, out value);
} while (value <= 0);

Console.WriteLine($"You entered {value}");
Output
Enter a positive number: -3
Enter a positive number: 0
Enter a positive number: 5
You entered 5
Production Trap:
Using while with a boolean flag to mimic do-while creates extra state and a risk of infinite loops from misplaced initialization.
Key Takeaway
Guarantee first-run execution by using do-while when the loop body generates the data the condition checks.

Consecutive Sums: When Correct Math Beats Clever Code

Consecutive sums—adding numbers from A to B without a loop—is a pattern that exposes the gap between brute-force thinking and arithmetic reasoning. The naive approach uses a for-loop accumulating integer by integer. The production-ready approach uses the arithmetic series formula: sum = (A + B) * (count) / 2, where count = B - A + 1. The first version is O(n) and fails under large ranges with integer overflow. The second is O(1) and forces you to think about division order to avoid truncation errors. In C#, integer division loses the fraction; you must ensure the product is even before dividing, or use long and divide at the end. This tiny pattern repeats everywhere: building cumulative metrics, pagination offsets, and time-series aggregates. Getting it wrong silently corrupts reports.

ConsecutiveSum.csCSHARP
1
2
3
4
5
6
7
// io.thecodeforge — csharp tutorial

int a = 1, b = 100;
long count = b - a + 1;
long sum = (a + b) * count / 2;

Console.WriteLine(sum);
Output
5050
Production Trap:
Multiplying before dividing with ints can overflow silently. Use long and compute (a + b) * count / 2 to preserve precision.
Key Takeaway
Replace iterative accumulation with the arithmetic series formula to avoid overflow and reduce O(n) to O(1).
● Production incidentPOST-MORTEMseverity: high

Infinite Loop Took Down Payment Processing for 45 Minutes

Symptom
Payment reconciliation job ran indefinitely, consuming 100% CPU on the batch server. Alerts fired for job timeout after 30 minutes.
Assumption
The team assumed the while loop condition would eventually become false because the remote API would return a different status each call.
Root cause
The loop used a while (status == Pending) condition but never updated status inside the loop body. The API was called once outside, and the loop repeated forever on the same stale value.
Fix
Moved the API call inside the loop, added a maximum iteration guard (maxAttempts), and used a break after timeout.
Key lesson
  • Every loop needs a guaranteed exit path that changes a loop variable or condition.
  • Add a safety counter (max retries) to any loop that depends on external state.
  • Code review policies should flag loops without obvious exit conditions.
Production debug guideSymptom → Action guide for the most common control flow failures4 entries
Symptom · 01
Application hangs, CPU at 100% for a specific request
Fix
Check logs for repeated output — likely an infinite loop. Attach a debugger or add temporary logging inside loop bodies. Look for unmodified loop variables.
Symptom · 02
Switch statement not matching expected values
Fix
Verify the input value type and any implicit conversions. In modern switch expressions, ensure the _ default arm is present to catch unexpected values.
Symptom · 03
IndexOutOfRangeException in a for loop
Fix
Check loop bounds: use < not <= for zero-based arrays. If using a dynamic collection, count items before the loop to avoid mid-loop resizing.
Symptom · 04
Condition always true or always false
Fix
Check operator precedence. Eg: if (x = 5) is assignment, not comparison. C# warns about this, but nullable or boolean types can still cause logic errors.
★ Quick Fixes for Control Flow BugsImmediate commands and code changes to resolve common control flow failures without restarting the service
Infinite loop detected in production
Immediate action
Kill the process with SIGTERM (if hanging) or restart the affected service. Then inspect the loop.
Commands
dotnet-counters monitor --process-id <pid> --counters System.Runtime[cpu-usage]
dotnet-dump collect --process-id <pid> (to capture memory dump for post-mortem)
Fix now
Add a max iteration guard: int guard = 0; while(condition && guard++ < 100_000) { / loop / }
Switch missing default case causing unhandled paths+
Immediate action
Check logs for unexpected input values. Add a logging statement in the default arm.
Commands
grep 'unexpected' /var/log/app/current.log
dotnet-trace collect --providers Microsoft-Windows-DotNETRuntime --process-id <pid>
Fix now
Add a default case that logs the unmatched value: _ => Log.Warning("Unexpected role: {Role}", role)
Foreach modifying collection throws InvalidOperationException+
Immediate action
Rollback the deployment that introduced the modifying code. The exception is by design to prevent undefined behavior.
Commands
kubectl rollout undo deployment/app -n production
git blame <file> to find the change
Fix now
Use a for loop with index and decrement when removing items: for (int i = list.Count - 1; i >= 0; i--)
Control Flow Constructs: When to Use Each
StatementBest Use CasePrimary Advantage
if / elseRange-based or complex boolean logicMaximum flexibility
switchMatching a single variable against many valuesReadability and performance (jump tables)
foreachReading every item in a collectionSafest and cleanest syntax; prevents index errors
forIterating with a known index or countGranular control over the iteration step
whileRepeating until a dynamic condition is metIdeal for polling or event-based logic

Key takeaways

1
Control flow dictates the execution path of your C# application.
2
Use 'foreach' by default for collections to ensure safe, readable iteration.
3
Leverage modern switch expressions for cleaner, functional-style code.
4
Always ensure loops have a guaranteed exit condition to prevent memory leaks or hangs.
5
Use 'using' for resource management to avoid orphaned connections and handles.

Common mistakes to avoid

5 patterns
×

Infinite Loops: Forgetting to increment the counter in a 'while' loop

Symptom
The program hangs indefinitely, CPU usage spikes, and the request/process never completes.
Fix
Always update the loop variable inside the body. Add a max iteration guard: int maxAttempts = 1000; while (condition && attempt < maxAttempts) { attempt++; }.
×

Off-by-One Errors: Using '<=' instead of '<' in a 'for' loop

Symptom
An IndexOutOfRangeException is thrown when accessing an array or list at index length.
Fix
Use i < array.Length for zero-based collections. Remember that array indices go from 0 to Length-1.
×

Using 'if' when 'switch' is better

Symptom
Code becomes deeply nested and hard to read, often spanning multiple screen heights.
Fix
Refactor the if-else chain into a switch statement or switch expression. If the condition compares a single variable against many constants, switch is the right tool.
×

Equality with Floats: Using '==' to compare floating-point numbers

Symptom
Conditions that should be true evaluate to false due to precision issues. E.g., 0.1 + 0.2 == 0.3 returns false.
Fix
Compare with a tolerance: Math.Abs(a - b) < 1e-9. Or use decimal for exact decimal arithmetic (financial calculations).
×

Modifying a collection while iterating with foreach

Symptom
An InvalidOperationException is thrown: "Collection was modified; enumeration operation may not execute."
Fix
If you need to add or remove items, use a for loop with index and adjust the index accordingly, or collect modifications in a separate list and apply after the loop.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is the difference between `while` and `do-while` loops in C#?
Q02SENIOR
How does pattern matching work in modern C# switch statements?
Q03JUNIOR
Explain the 'Short-Circuiting' behavior of logical operators like `&&` a...
Q04SENIOR
What is the 'Default' case in a switch statement, and why is it crucial ...
Q05SENIOR
How would you refactor a deeply nested if-else structure to improve code...
Q01 of 05JUNIOR

What is the difference between `while` and `do-while` loops in C#?

ANSWER
while checks the condition before executing the loop body, so the body may never run. do-while checks the condition after the body executes, guaranteeing at least one iteration. Use do-while when you need to perform an action before checking whether to repeat (e.g., reading user input until valid).
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
When should I use 'switch' instead of 'if-else'?
02
Is 'foreach' slower than a standard 'for' loop?
03
Can I exit a loop early?
04
What is the difference between 'break' and 'continue'?
05
Why does 'foreach' throw an exception when I modify the collection?
N
Naren Founder & Principal Engineer

20+ years shipping production .NET services in enterprise systems. Notes here come from systems that actually shipped.

Follow
Verified
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
🔥

That's C# Basics. Mark it forged?

10 min read · try the examples if you haven't

Previous
C# Data Types and Variables
3 / 11 · C# Basics
Next
Methods and Parameters in C#