AlgoMaster Logo

Iterator Pattern (yield)

High Priority28 min readUpdated June 6, 2026

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 common failure modes.

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, the cost is negligible. For a product catalog with five million rows, both costs hurt.

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 CountUp(4) is called. 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.

CountUp(4) itself does no counting. It produces a sequence object. The counting happens during enumeration.

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. Knowing it exists explains a lot of the observable behavior: 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).

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.

The generated state machine is one allocation per call to the iterator method. Inside the body, no per-element allocation happens beyond what the body itself does. That is the main reason iterators beat building a list: they trade O(n) intermediate storage for one small class instance.

Deferred (Lazy) Execution

The body not running until enumeration starts has consequences. 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 complicates debugging.

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 property makes iterators safe to use over very large 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 ends after the second value.

yield break is what return does in an ordinary method. A bare return is not allowed inside an iterator method, because the compiler needs to know whether the intent is "end the sequence" (yield break) or "produce this value and continue" (yield return). A plain return inside an iterator method is a compile error.

Multiple yield break statements are allowed, 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 correct 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 continues without yielding.

Yielding works 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.

Each composed iterator stage adds one state-machine allocation and a small per-element overhead for the chain of MoveNext calls. For long pipelines or hot inner loops, that overhead is measurable. For most application code (catalog filters, report generators, paginators), it is invisible compared to the cost of the data fetch.

The same shape works with infinite or very 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 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 internally. Writing a custom version side by side with the built-in one shows the relationship clearly.

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 includes optimizations (specializing 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.

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 goal is not to reimplement LINQ but to show that LINQ's lazy behavior is yield applied consistently. Understanding yield explains why var q = list.Where(...) does no work until something iterates q.

Making a Custom Collection Enumerable

yield works for implementing GetEnumerator on a custom class. Any type with a GetEnumerator() method returning a type with MoveNext and Current is foreach-able. Implementing IEnumerable<T> makes that explicit and provides LINQ support.

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.

IEnumerable<T> extends IEnumerable, so both GetEnumerator methods are required. The non-generic one usually forwards to the generic one, as shown.

Once Cart implements IEnumerable<CartItem>, any LINQ method works 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 that matter" 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.

To walk a sequence twice, 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.

ToList() allocates a list and copies every element. Use it when walking the same sequence multiple times, when indexing or Count is needed cheaply, or when the result must be detached from a source that might change. It is not the right default.

Side Effects Are Deferred

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 "the counter is wrong" or "the 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 produce a snapshot. If the source changes while 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 unpredictable values, skipping items, or yielding the same item twice depending on how the change interacted with the internal indices.

Custom iterators inherit this hazard. If the iterator walks a list and the list is modified while the iterator is paused between yields, the next MoveNext may throw or return inconsistent results. The general rule: do not mutate the source collection while iterating it. Materialize first if necessary.

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 purpose of supporting finally in iterators: it provides a place to release resources even when the consumer abandons the sequence. Opening a file in the iterator body and yielding lines closes the file whether the caller reads them all or breaks after one.

Catching exceptions inside an iterator is allowed, 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 do not need to handle it.

Calling GetEnumerator and MoveNext by hand requires explicit cleanup via using (or an explicit dispose):

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 rule: foreach handles disposal automatically. Direct GetEnumerator calls do not. When driving an enumerator by hand (which is rare), wrap it in using or call Dispose explicitly.

Putting It Together: Paginating Orders Lazily

A larger example combines several 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.

Quiz

Iterator Pattern & yield Quiz

10 quizzes