Mert Özen Further With Every Line
Backend C# Fundamentals — #09

Loops in C#: for, while, do-while and foreach

Mert Özen Nov 7, 2026 10 min 7 views
Loops in C#: for, while, do-while and foreach

The four loops in C# and when to use each: for, while, do-while and foreach. break and continue, a clean way out of nested loops, the infinite loop pattern, and why changing a collection while walking it throws an exception.

Version note: The code in this post uses C# 10 and the .NET 6 APIs; it was built and run with the .NET 10 SDK.

Guess First

The customer's cart holds three books and we want to take "Emma" out of it. We walk the cart with foreach and remove the book from the list when we find it. The code compiles. Does it print 2 at the end, 3, or does something else happen?

var cart = new List<string> { "Dune", "Emma", "Ulysses" };

foreach (string title in cart)
{
    if (title == "Emma")
    {
        cart.Remove(title);
    }
}

Console.WriteLine(cart.Count);

No number gets printed; the program dies with an exception:

Unhandled exception. System.InvalidOperationException: Collection was modified; enumeration operation may not execute.
   at System.Collections.Generic.List`1.Enumerator.MoveNext()
   at Program.<Main>$(String[] args) in /src/Bookstore/Program.cs:line 3

"Emma" really was removed from the list, but foreach stopped the moment it tried to move to the next element. List<T> increments an internal version counter on every change; the enumerator that foreach uses checks that counter at every step, and when it sees that the list it is walking has changed, it refuses to continue. It does not let you pull the rug out from under its feet while it walks. In this post we look at the four loops in C#, at break and continue, at the ways out of a nested loop, and at the right fix for this trap.

The for Loop

You use for when you know how many times to repeat, or when you need an index:

string[] titles = { "Dune", "Emma", "Ulysses" };

for (int i = 0; i < titles.Length; i++)
{
    Console.WriteLine($"{i + 1}. {titles[i]}");
}

for (int i = titles.Length - 1; i >= 0; i--)
{
    Console.Write(titles[i] + " ");
}
Console.WriteLine();

for (int i = 0; i < 10; i += 3)
{
    Console.Write(i + " ");
}
Console.WriteLine();
1. Dune
2. Emma
3. Ulysses
Ulysses Emma Dune
0 3 6 9

There are three parts inside the parentheses, separated by semicolons. The first is the initializer: the loop variable is declared once. The second is the condition: it is checked before every iteration, and the loop ends as soon as it is false. The third is the step: it runs at the end of every iteration. The second loop counts down from the end and the third moves in steps of three; you can write any expression in the three parts.

A variable declared in the parentheses of a for lives only inside the loop:

for (int i = 0; i < 3; i++)
{
}

Console.WriteLine(i);
Program.cs(5,19): error CS0103: The name 'i' does not exist in the current context

That is a good thing. It lets you reuse the name i in two loops one after the other, and it stops you from touching a stale index value by accident after the loop is over. If you really need the last value after the loop, you declare the variable before the loop.

The while Loop

If you do not know in advance how many times you will repeat, and all you can say is "as long as this condition holds", while is more natural. How many days until a stock that sells 7 books a day runs out:

int stock = 20;
int day = 0;

while (stock > 0)
{
    day++;
    stock -= 7;
    Console.WriteLine($"Day {day}: {Math.Max(stock, 0)} left");
}
Day 1: 13 left
Day 2: 6 left
Day 3: 0 left

while checks the condition at the very start. If the stock had been 0 to begin with, the body would not have run even once. The one thing to watch is that the body contains a change that eventually makes the condition false; here stock -= 7 does that job. Forget that line and the loop spins forever.

The do-while Loop

do-while checks the condition at the end; the body always runs at least once. Menus in console applications are the classic example, because you have to show the menu before the user has picked anything:

string choice;

do
{
    Console.WriteLine("1) List books  2) Add book  q) Quit");
    choice = Console.ReadLine() ?? "q";
} while (choice != "q");

Console.WriteLine("Bye");

When I run the program and type x, 2 and q in turn, the console looks like this:

1) List books  2) Add book  q) Quit
x
1) List books  2) Add book  q) Quit
2
1) List books  2) Add book  q) Quit
q
Bye

Console.ReadLine() can return null when the input stream ends; that is why its return type is string?. With ?? "q" we leave the loop in that case too. We will open up the topic of null in a separate post. The semicolon at the end of while (choice != "q"); is required here; in a moment we will see what kind of disaster the same semicolon causes in a while loop.

The foreach Loop

If you are going to walk every element of a collection from start to end and do not need an index, foreach is the most readable option:

using System.Globalization;

CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;

decimal[] prices = { 12.50m, 8.90m, 21.00m };
decimal total = 0m;

foreach (decimal price in prices)
{
    total += price;
}

Console.WriteLine(total);

foreach (char c in "ISBN")
{
    Console.Write(c + "-");
}
Console.WriteLine();

var stockByTitle = new Dictionary<string, int> { ["Dune"] = 3, ["Emma"] = 0 };
foreach (KeyValuePair<string, int> entry in stockByTitle)
{
    Console.WriteLine($"{entry.Key}: {entry.Value}");
}
42.40
I-S-B-N-
Dune: 3
Emma: 0

foreach works on arrays, on List<T>, on the characters of a string, on a Dictionary, and in short on anything that is IEnumerable. In a Dictionary every element arrives as a KeyValuePair. Since there is no index arithmetic, there is also no chance of an "off by one" mistake.

The loop variable of foreach is read-only:

decimal[] prices = { 12.50m, 8.90m, 21.00m };

foreach (decimal price in prices)
{
    price = price * 0.9m;
}
Program.cs(5,5): error CS1656: Cannot assign to 'price' because it is a 'foreach iteration variable'

Even if you could assign a new value to the variable, it would not change the value in the array; price is a copy of the element. If you want to update the prices in the array, you need an index, which means for. We will talk about the value type and reference type difference behind this copying in a separate post later.

break and continue

continue abandons the current iteration and moves to the next one. break ends the loop completely:

int[] stock = { 3, 0, 5, 0, 2 };

for (int i = 0; i < stock.Length; i++)
{
    if (stock[i] == 0)
    {
        continue;
    }

    Console.WriteLine($"Shelf {i}: {stock[i]} copies");
}

int firstEmpty = -1;
for (int i = 0; i < stock.Length; i++)
{
    if (stock[i] == 0)
    {
        firstEmpty = i;
        break;
    }
}
Console.WriteLine($"First empty shelf: {firstEmpty}");
Shelf 0: 3 copies
Shelf 2: 5 copies
Shelf 4: 2 copies
First empty shelf: 1

The first loop skips the shelves whose stock is zero. Think of continue as the short way to say "this element is not my concern": it saves you from wrapping the rest of the body in one big if. The second loop stops when it finds the first empty shelf; once you have found what you are looking for, there is no point in walking the rest of the list.

Getting Out of a Nested Loop

break only ends the nearest loop it sits in. If we search the books on the shelves with two loops and want to leave both when we find the book, that is a problem:

string[][] shelves =
{
    new[] { "Dune", "Emma" },
    new[] { "Ulysses", "Hamlet", "Beloved" },
    new[] { "Ivanhoe" }
};

string target = "Hamlet";
bool found = false;

for (int shelf = 0; shelf < shelves.Length; shelf++)
{
    for (int slot = 0; slot < shelves[shelf].Length; slot++)
    {
        Console.WriteLine($"Checking {shelf}/{slot}");
        if (shelves[shelf][slot] == target)
        {
            found = true;
            break;
        }
    }

    if (found)
    {
        break;
    }
}

Console.WriteLine(found);
Checking 0/0
Checking 0/1
Checking 1/0
Checking 1/1
True

The break in the inner loop only ended the scan of that shelf; to leave the outer loop we had to keep a found flag and check it in the outer loop too. It works, but even at two levels the code gets crowded. My preference is to move the search into a method and return as soon as the result is found:

string[][] shelves =
{
    new[] { "Dune", "Emma" },
    new[] { "Ulysses", "Hamlet", "Beloved" },
    new[] { "Ivanhoe" }
};

Console.WriteLine(FindBook(shelves, "Hamlet"));
Console.WriteLine(FindBook(shelves, "Emma"));
Console.WriteLine(FindBook(shelves, "Odyssey"));

static string FindBook(string[][] shelves, string title)
{
    for (int shelf = 0; shelf < shelves.Length; shelf++)
    {
        for (int slot = 0; slot < shelves[shelf].Length; slot++)
        {
            if (shelves[shelf][slot] == title)
            {
                return $"shelf {shelf}, slot {slot}";
            }
        }
    }

    return "not found";
}
shelf 1, slot 1
shelf 0, slot 1
not found

return ends the method, and with it every loop inside, in one move. No flag, no extra check, and the method's name tells you what it does. In C# you can also jump to a label with goto, but you will see it very rarely in real code and it stops everyone who reads it. With C# 15 it also becomes possible to label loops and point break and continue directly at an outer loop; we will see that in the C# Versions series.

Infinite Loops

Sometimes when a loop ends is not known at the start but somewhere in the middle of the body. Think of a console application that takes commands until the user types "quit". The established pattern for this is while (true) with a break inside:

while (true)
{
    Console.Write("Command: ");
    string? command = Console.ReadLine();
    if (command is null or "quit")
    {
        break;
    }

    Console.WriteLine($"Running {command}");
}

Console.WriteLine("Loop finished");

When I type list, add and quit in turn:

Command: list
Running list
Command: add
Running add
Command: quit
Loop finished

is null or "quit" is the logical pattern we saw in the previous post; we also leave the loop if the input stream ends. You may see the same pattern written as for (;;) in older code; both do the same thing. An infinite loop is not bad in itself; servers, game loops and background jobs that listen to a queue are infinite loops at heart. What matters is that the way out is clear and visible.

Common Pitfalls

Putting a semicolon after while. The code below compiles and never finishes:

int count = 0;

while (count < 3);
{
    count++;
}

Console.WriteLine(count);
Program.cs(3,18): warning CS0642: Possible mistaken empty statement

The line while (count < 3); is a loop with an empty body. Because count never increases, the condition stays true and the program spins on that line forever; the braced block below it is just an ordinary block that has nothing to do with the loop. The compiler's CS0642 warning says exactly that. When you see this warning, delete the semicolon before looking at anything else.

Taking the bound one too far. Writing <= instead of < is the classic "off-by-one" mistake:

string[] titles = { "Dune", "Emma", "Ulysses" };

for (int i = 0; i <= titles.Length; i++)
{
    Console.WriteLine(titles[i]);
}
Dune
Emma
Ulysses
Unhandled exception. System.IndexOutOfRangeException: Index was outside the bounds of the array.
   at Program.<Main>$(String[] args) in /src/Bookstore/Program.cs:line 5

Indexes start at 0, and the last valid index is Length - 1. If you are going to walk the whole collection, using foreach removes this mistake completely.

Using a double as a loop counter. Adding 0.1 ten times does not give you exactly 1.0, because of the floating-point error we saw in the numbers post:

using System.Globalization;

CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;

int steps = 0;
for (double x = 0.0; x != 1.0; x += 0.1)
{
    steps++;
    if (steps > 15)
    {
        Console.WriteLine($"Still running, x = {x:R}");
        break;
    }
}
Still running, x = 1.5000000000000002

Because the condition is !=, the loop passed right by 1.0 without ever hitting it. Without the 15-step safety net in the example, it would have run forever. Keep the counter an int and compute the decimal value in the body; if you really must, use < instead of != in the condition.

Changing a collection while walking it. The exception from the start of the post has two correct fixes. Walk with an index from the end to the start and remove, or leave the work to List<T> itself:

var cart = new List<string> { "Dune", "Emma", "Ulysses", "Emma" };

for (int i = cart.Count - 1; i >= 0; i--)
{
    if (cart[i] == "Emma")
    {
        cart.RemoveAt(i);
    }
}
Console.WriteLine(string.Join(", ", cart));

var cart2 = new List<string> { "Dune", "Emma", "Ulysses", "Emma" };
cart2.RemoveAll(title => title == "Emma");
Console.WriteLine(string.Join(", ", cart2));
Dune, Ulysses
Dune, Ulysses

Walking from the end matters, because when you remove an element while walking from the start, the elements after it shift one to the left and you skip the one right behind it. RemoveAll states the intent in a single line; the title => title == "Emma" inside it is a lambda, and we will look at those in detail later.

A Small Experiment

★☆☆ Using for and continue, print the numbers from 1 to 100 that are divisible by 7.

★★☆ Add up the prices in a decimal[] until the total passes 50, and print at which book the limit was crossed. Write it first with for and break, then with while; which one reads better?

★★★ Write a menu with do-while: 1 adds a book (reads its title with Console.ReadLine()), 2 prints the list, 3 removes the book with the given title, q quits. Make sure the removal does not run into the exception from the start of the post.

The thing we walked most in these loops was arrays, but we always used them as if they were just there. In the next post we get to know arrays up close: single and multidimensional arrays, jagged arrays, the methods of the Array class, Index and Range (^1, ..) from C# 8, and why an array is a reference type.