Skip to content
Free public beta until December 4, 2026: every course is open, no card needed. After that, the first chapter of each course stays free and full access is a one-time $39 ($19 for up to 100 founding members chosen for helpful feedback). Your progress is kept. How the beta works

Every way to loop over data

while, do, for, foreach, LINQ, yield return, recursion, await foreach and parallel loops. Which one says what you mean.

Pick the loop that says the most about what you're doing.

  • Visit every item, in order → foreach. It works on arrays, lists, dictionaries, strings and anything else that's a sequence, and there's no index to get wrong.
  • You need the index, you step by more than one, or you walk backwards → for.
  • You don't know how many passes (re-prompt until the input is valid, read until the file ends) → while. If the body must run at least once, like a menu, do ... while.
  • You're building a new sequence from an old one (keep the paid orders, turn each into a line of text, total them) → LINQ: Where, Select, Sum. Call ToList() when you need the answer now and more than once.
  • You're producing items one at a time for someone else to loop over → an iterator method with yield return.
  • The data is nested (folders in folders, replies to replies) → recursion. For a flat run of steps, the loop is simpler and can't overflow the stack.
  • Each item arrives asynchronously (pages from an API, rows from a database) → await foreach over an IAsyncEnumerable<T>.
  • Many independent, CPU-heavy items → Parallel.ForEach, and only after you've measured that a plain loop is too slow.

Whatever you pick, don't add or remove items from a collection while foreach is walking it.

The four loop statements

Check first, or run once and then check. Loops that never end, and how to spot them.

Section

for and foreach

A counter you control, or every item in turn. Off-by-one errors and loop-variable scope.

Leave the loop, skip one pass, or leave the whole method, and when a flag reads better.

for when you need the index, foreach when you don't, and why foreach can't change the elements.

Formal. A List<T>'s enumerator is invalidated by any change to the list while a foreach loop is running over it, including Add, Remove, and even replacing an item with list[i] = x; the loop's next step throws InvalidOperationException ("Collection was modified"). Dictionary<TKey,TValue> and HashSet<T> are more permissive on modern .NET (since .NET Core 3.0): Remove and overwriting the value of an existing key are allowed during enumeration, but Add (and any change that adds a new key) still throws.

In plain English. A foreach loop is walking through a collection. If you add or remove items while it walks, it can lose its place, so .NET stops the program instead of guessing. A list is strict about this: change it in any way and the loop fails. A dictionary or set lets you remove as you go, but adding new entries still fails.

Example. foreach (var n in numbers) if (n < 0) numbers.Remove(n); throws on a List<int>. numbers.RemoveAll(...), a loop over a copy ([.. numbers]), or collecting the changes and applying them after the loop all work.

Why this rule exists. Failing loudly at the first bad step is far better than silently skipping or repeating items.

Adding or removing items while foreach walks a list throws InvalidOperationException.

Sequences and LINQ

Anything foreach can walk is an IEnumerable, and yield return lets you write one.

Keep the items that pass a test, and turn each item into something else, without writing the loop.

Find, FindAll, Exists and RemoveAll do the loop for you when you hand them a Predicate.

Formal. Operators that return a sequence (Where, Select, OrderBy, Take, GroupBy …) do no work when called; they return a query object. The query runs when something enumerates it (foreach, ToList, Count, First …), against the source as it is at that moment, and it runs again in full on every enumeration, repeating any side effects in its lambdas. Operators that return a single value or a materialized collection (Count, Sum, First, Any, ToList, ToArray, ToDictionary) execute immediately.

In plain English. Writing var adults = people.Where(p => p.Age >= 18); doesn't find any adults yet. It writes down a recipe for finding them. The recipe is followed only when you ask for the results, and it is followed again each time you ask. Add a person to the list first, and the next run includes them. Call ToList() when you want the answers frozen at one moment.

Example. With var q = nums.Where(n => n > 1); over [1, 2, 3], adding 4 to nums and then calling q.Count() gives 3, not 2. Calling q.Count() twice runs the filter lambda over the list twice.

Why this rule exists. Deferring lets queries be composed cheaply, stream large or endless sources, and stop early (Take, First) without touching every item.

A query runs when you loop over it, not when you write it. ToList() runs it now.

Other shapes of repetition

A method that calls itself fits nested data. For a straight run of steps, the loop wins.

A loop whose items arrive over time, with cancellation and early exit.

Spread independent, CPU-heavy items across cores. Worth it only for real work and no shared state.