AlgoMaster Logo

Iterator Pattern (yield)

Last Updated: May 17, 2026

15 min read

The yield keyword turns an ordinary method into an iterator: a method whose body produces a sequence of values one at a time, on demand, instead of building a list and returning it. The compiler rewrites the method into a small state machine that pauses after each value and resumes on the next request. This lesson covers how yield return and yield break work, the deferred-execution model they create, how to compose iterators, and the failure modes that catch people out.

Why yield Exists

Returning IEnumerable<T> the old way meant building a List<T>, filling it, and returning it. That works, but it forces the caller to wait for the whole list before reading the first element, and it allocates storage for every result whether the caller reads them all or stops after the third one. For an order history with twenty entries, nobody cares. For a product catalog with five million rows, both costs hurt.

Here is the eager version, building a list of order IDs that match a status:

The method walks the whole input, builds a fresh List<int>, and hands it back. By the time the caller's foreach reads the first ID, the work is already done and the list already exists on the heap. The caller can do nothing about that. Even if the loop breaks after one iteration, the entire matching list was computed.

The iterator version replaces the list with yield return:

Same output, very different execution. No List<int> is allocated. Each time the foreach in Main asks for the next value, control jumps back into FindOrderIds right after the last yield return, runs until it hits the next one, and pauses again. If Main breaks out of the loop after the first ID, the rest of the orders never get inspected.

The basic rule: a method that contains a yield return or yield break and is declared to return IEnumerable<T>, IEnumerator<T>, IEnumerable, or IEnumerator is an iterator method. This lesson is about the body shape: what yield does inside an iterator method.

A First Iterator: CountUp

The smallest useful iterator method takes a count and yields the integers from 1 to n.

CountUp looks like a normal method, but the compiler treats it differently because of the yield return. It does not actually execute the for loop the moment you call CountUp(4). Instead, it returns an object that implements IEnumerable<int>. The body runs piece by piece as the caller pulls values out.

Inside the foreach, the runtime asks the enumerator object for one value at a time. Each request runs the body up to the next yield return, hands the yielded value back, and remembers where it stopped. The next request resumes from that point.

This is the part most people miss the first time: CountUp(4) itself does no counting. It just produces a sequence object. The counting happens when you enumerate.

Notice that the line [body] CountUp(2) starting does not print right after CountUp(2) is called. It prints only when the foreach begins. That is the heart of deferred execution: the iterator body does not run on the call, it runs as the caller pulls.

The State Machine Behind yield

The compiler does real work to make yield look effortless. When it sees an iterator method, it generates a hidden class that implements IEnumerator<T> (and IEnumerable<T> for the entry point). That class has a state field, a current-value field, and a MoveNext method whose body is the iterator method rewritten as a switch over the state.

Roughly, this iterator:

becomes something close to this (simplified):

The state field tracks where the body should resume on the next MoveNext. Each yield return becomes a "save the value, advance the state, return true" sequence. When the body finishes, MoveNext returns false. The values you yield are not stored in a buffer. There is only one slot, _current, and it gets overwritten on every step.

You do not write this class yourself. The compiler writes it for you. But knowing it exists explains a lot of the behavior you observe: why iterator bodies pause and resume, why foreach needs both MoveNext and Current, and why local variables inside an iterator method survive across yields (they become fields on the generated class).

Here is the call sequence between a foreach loop and the iterator's state machine for CountUp(3):

Each box on the right represents the iterator method paused at a known point. MoveNext resumes it, runs until the next yield, and returns. There is no separate thread, no callback, no async. The iterator method and the caller share one thread and take turns.

Deferred (Lazy) Execution

The fact that the body does not run until enumeration starts has consequences that surprise people. The most common one: code that looks like it should run on the call actually does not.

The argument check is inside the iterator body, so it does not run when EvensUpTo(-5) is called. It runs the first time something calls MoveNext on the returned enumerator, which is when the foreach starts. The exception then surfaces at the foreach, not at the call site, which is confusing to debug.

To make argument checks eager, split the iterator into a public method that validates and a private one that yields:

The outer EvensUpTo is a regular method now, so its body runs immediately. The validation throws on the call. Only if validation passes does it call the inner iterator, which still does the lazy work. This split is a standard idiom in BCL-style code.

The other side of laziness: only as much work as the caller needs is done.

The loop breaks when x == 3. The iterator only produced three values, not ten. The fourth through tenth iterations of the for inside Tracked never run, because nothing ever called MoveNext again. This is the property that makes iterators safe to use over enormous or infinite sources.

Both versions hand the caller three items. The eager version did seven extra units of work that nobody asked for. The lazy version did exactly three.

yield break: Ending the Sequence Early

yield return produces a value. yield break ends the sequence. After a yield break, the next MoveNext returns false, and the body never runs again for that enumerator.

The iterator yields IDs until it sees a cancelled order, then yield break terminates the sequence. Orders 204 and 205 are never inspected, even though they exist in the array. From the caller's point of view, the sequence simply ends after the second value.

yield break is what return would do in an ordinary method. You cannot use a bare return inside an iterator method, because the compiler needs to know whether you mean "end the sequence" (yield break) or "produce this value and continue" (yield return). A plain return inside an iterator method is a compile error.

You can have multiple yield break statements, just like multiple return statements:

Three guards, three yield break paths, one happy path that yields values. The page-past-the-end case prints nothing under the heading, which is the right shape for a paginator: when there is no data, the sequence is empty, not an error.

Multiple Yields and Conditional Yields

An iterator method can have any number of yield return statements, in any positions the control flow allows. Each one is a potential pause point. The state machine handles all of them.

Three yield return statements, the last one chosen by an if/else. The sequence has three values for every product, regardless of which status branch ran. The state machine does not care about the structure of your code, only about reaching the next yield.

Conditional yields let you skip values:

This is the iterator version of Where. The body walks the source once, and only items that pass the test reach the caller. Items that fail produce no value at all, the loop just goes around without yielding.

You can yield from inside any control structure: if, for, foreach, while, switch, try (with restrictions covered below). The compiler tracks all the points where the body might pause and turns them into states.

Iterator Composition

Iterators compose well because consuming one iterator inside another is just a foreach. The outer iterator pulls from the inner one and yields onward.

Two simple filters plug together. ShippedOrders pulls from all and yields shipped ones. OverAmount pulls from ShippedOrders and yields those above the amount threshold. The data flows through the pipeline lazily: each MoveNext at the outer level triggers one or more MoveNext calls down the chain, until somebody produces a match.

Nothing gets buffered between stages. There is no intermediate list of "shipped orders" sitting in memory. The orders walk through the pipeline one at a time, and only the final ones the caller asks for actually pay the cost of being inspected.

The same shape works with infinite or extremely large sources, because no stage materializes the whole stream.

NaturalNumbers is an infinite iterator. By itself, iterating it would never finish. But Multiples only pulls from it on demand, and Take(5) only pulls from Multiples until it has five values. The system happily reads a few dozen naturals, filters down to the five multiples of seven, and stops. The infinite source is fine because nothing forces it to produce more than is needed.

Writing Your Own Where vs Enumerable.Where

LINQ's standard operators are iterators under the hood. Writing your own version side by side with the built-in one is the clearest way to see the relationship.

Manual Where:

Eight lines of body, including the signature, for a function that is laziness, filtering, and generics in one. The built-in Enumerable.Where does the same thing with the same shape:

Same result. The BCL implementation is a touch more clever (it specializes for arrays and lists to skip an allocation in common cases), but the core idea is identical: a single iterator method that walks the source and yields the matches.

Here is a small table of LINQ methods and their iterator equivalents:

LINQ methodIterator body in plain C#
Where(p)foreach (var x in src) if (p(x)) yield return x;
Select(f)foreach (var x in src) yield return f(x);
Take(n)int i = 0; foreach (var x in src) { if (i++ >= n) yield break; yield return x; }
Skip(n)int i = 0; foreach (var x in src) { if (i++ < n) continue; yield return x; }
Concat(other)foreach (var x in src) yield return x; foreach (var x in other) yield return x;

Each one is a few lines of iterator code. The point is not that you should reimplement LINQ. The point is that LINQ's lazy behavior is not magic, it is yield applied consistently. When you understand yield, you understand why var q = list.Where(...) does no work until something iterates q.

Making a Custom Collection Enumerable

You can use yield to implement GetEnumerator on your own class. Any type with a GetEnumerator() method returning a type with MoveNext and Current is foreach-able. Implementing IEnumerable<T> makes that explicit and gives you LINQ for free.

The Cart class exposes itself as a sequence of non-zero-quantity items. The GetEnumerator method uses yield return to filter the underlying list. From the outside, Cart looks like a collection of CartItem. Inside, the filtering happens lazily as the caller iterates.

A subtle detail: IEnumerable<T> extends IEnumerable, so you have to provide both GetEnumerator methods. The non-generic one usually just forwards to the generic one, which is the pattern shown.

Once Cart implements IEnumerable<CartItem>, you can use any LINQ method on it:

Sum and Count work on Cart because Cart is IEnumerable<CartItem>. The zero-quantity voucher is invisible to both, because the iterator inside GetEnumerator skips it. The "items I care about" rule is encoded once, inside the collection, and every consumer benefits.

Re-enumeration Cost

An iterator method does not cache anything. Each new foreach over the same IEnumerable<T> starts the body from the top.

Each foreach calls GetEnumerator on the IEnumerable<int>, which produces a fresh state machine starting at state 0. The body runs end to end both times. If the body did expensive work (calling a database, reading a file, hitting a remote API), that cost doubles.

The fix when you want to walk a sequence twice is to materialize it once:

ToList() enumerated the iterator once, eagerly, and stored the results in a list. Both subsequent loops walk the list, which is cheap and does not re-run the iterator body. The body's "starting" message prints exactly once.

Side Effects Are Deferred

This is the bug that bites people who treat iterator methods like normal methods. Side effects inside an iterator body do not happen on the call, they happen during enumeration.

The Calls++ line is inside the iterator body. Calling RecordAndYield does not run it. Calling it twice without iterating produces two enumerables, both with the side effect still deferred. The increment runs only when foreach starts pulling values.

This usually surfaces as "my counter is wrong" or "my log line never fires" bugs. The fix is the same split-method idiom from earlier: do the side effect in a regular wrapper, and have it return the iterator.

Now RecordAndYield is a regular method. The increment runs on the call. The inner local function is the iterator, and it does its lazy work only when something iterates.

Mutating the Source During Enumeration

The iterator pattern in C# does not give you a snapshot. If the source changes while you are iterating, the iterator sees the changes, and most BCL collections throw InvalidOperationException if their structure changes mid-enumeration.

List<T>'s enumerator notices that the list's version number changed between calls and refuses to continue. This is a safety feature, not a bug. The alternative would be returning surprising values, skipping items, or yielding the same item twice depending on how the change interacted with the internal indices.

Your own iterators inherit this hazard. If your iterator walks a list, and someone modifies the list while the iterator is paused between yields, the next MoveNext may throw or return inconsistent results. The general rule is: do not mutate the source collection while iterating it. Materialize first if you need to.

The foreach iterates a fresh list created by ToList(). Mutating the original prices afterwards has no effect on the in-flight iteration, and no exception is thrown.

Exception Handling in Iterators

try/catch/finally works inside iterator methods, with two notable rules. First, yield return cannot appear inside a catch block or a finally block. The compiler rejects that. Second, the finally block runs when the enumerator is disposed, which is what foreach does automatically when its loop ends (normally, by break, or by an exception).

In both runs, the finally block runs exactly once per enumeration, after the last value the caller consumed. When the loop finishes normally, the iterator exits its body, the runtime calls Dispose on the enumerator, and the finally runs. When the loop breaks early, the foreach still calls Dispose on the way out (it is written into the generated code), the Dispose cancels the paused iterator, and the finally runs there too.

This is the whole point of supporting finally in iterators: it gives you a place to release resources even when the consumer abandons the sequence. If you open a file in the iterator body and yield lines, the file gets closed whether the caller reads them all or breaks after one.

You can also catch exceptions inside an iterator, as long as the try block does not contain a yield return whose handler is in a catch:

The try/catch wraps only the parse call, not the yield return. Bad inputs are skipped via continue. Good inputs flow through and get yielded after the try block ends. This shape (try around the risky line, yield outside) is the standard way to filter errors inside an iterator.

IDisposable on Iterators

The enumerator object generated for an iterator method implements IDisposable. That is how the finally mechanism works: Dispose runs the unfinished finally blocks. foreach calls Dispose automatically when its loop ends, so most consumers never have to think about it.

You only need to be careful if you call GetEnumerator and MoveNext by hand. In that case, you are responsible for the using (or explicit dispose) that triggers cleanup:

The manual loop reads one value and the using block disposes the enumerator. Disposal forces the paused iterator to wake up, jump out of its try, and run the finally. Without the using, the iterator would stay paused forever (or until the garbage collector ran its finalizer, which is not a guarantee about timing).

The shape worth remembering: foreach handles disposal for you. Direct GetEnumerator calls do not. If you ever find yourself driving an enumerator by hand (which is rare), wrap it in using or call Dispose explicitly.

Putting It Together: Paginating Orders Lazily

Here is a slightly larger example that combines several of the ideas: argument validation, lazy enumeration, a custom iterator-backed collection, and composition. It paginates an order history without ever materializing the whole catalog.

Every piece in this code is an iterator. OrderHistory.GetEnumerator is one. ByStatus is one. Page is one (with the eager-validation wrapper splitting argument checks from the lazy body). The pagination calls walk the underlying list only as far as the page boundary plus the page size, and stop. For the second page, the iterator skips the first two shipped orders and yields the next two before yield break ends it.

Nothing in this code ever holds more than one order in flight, and the validation of the page index runs immediately at the call site, not at the start of the foreach. That is the production-shaped use of yield: tight memory, predictable failures, composable filters.

Summary

  • An iterator method is any method whose body contains yield return or yield break and whose return type is IEnumerable<T>, IEnumerator<T>, or the non-generic equivalents.
  • The compiler turns iterator methods into hidden state-machine classes. Local variables become fields, the body becomes a MoveNext method, and each yield is a pause point.
  • Execution is deferred: calling an iterator method only constructs the state machine. The body runs piece by piece as the caller pulls values with foreach or by calling MoveNext directly.
  • yield break ends a sequence early. A plain return is a compile error inside an iterator method.
  • Iterators compose: one iterator method can foreach over another, and the data flows through the pipeline lazily without buffering between stages. LINQ is built on this pattern.
  • Re-enumeration is not free. Each new foreach starts the body from the top. Materialize with ToList or ToArray when you need to walk the same sequence twice.
  • Side effects in iterator bodies are deferred too. Put eager work in a wrapper method that returns the iterator if you need it to run on the call.
  • try/finally inside iterators is supported. The finally runs on Dispose, which foreach calls automatically, so resource cleanup is safe even when the consumer reads only part of the sequence.

The next lesson, _Collection Expressions_, covers the C# 12 syntax for building collections from literal element lists, including spread elements and the new target-type rules that decide which concrete collection type the expression produces.