Skip to main content
SEMastery
.NET Corebeginner

C# yield return Statement: A Simple Guide With Real Examples

Learn the C# yield return statement the easy way. Understand iterators, lazy evaluation, and deferred execution with simple examples, diagrams, and tables.

11 min readUpdated May 22, 2026

A snack stall that makes one samosa at a time

Picture a small samosa stall near your school. The stall owner does not fry one hundred samosas in the morning and pile them up. Instead, he fries one samosa, hands it to the next person in line, and waits. Only when the next customer asks does he fry the next one.

This is smart. He never wastes oil. He never makes samosas nobody wants. If only three students show up, he fries only three.

The C# yield return statement works in exactly the same way. It lets a method produce a sequence one value at a time, only when someone actually asks for the next value. The method does the work, hands you one item, and then pauses politely until you come back for more.

That single idea makes your code use less memory, start faster, and stay clean. Let us build the whole picture step by step.

What is an iterator method?

A method that contains yield return is called an iterator method. You do not write the looping machinery yourself. The C# compiler builds it for you behind the scenes.

Here is the smallest possible example. It gives back the numbers 1, 2, and 3.

public static IEnumerable<int> GetSmallNumbers()
{
    yield return 1;
    yield return 2;
    yield return 3;
}
 
// Using it:
foreach (int number in GetSmallNumbers())
{
    Console.WriteLine(number); // prints 1, then 2, then 3
}

Notice three things. The return type is IEnumerable<int>, not int. There is no new List<int>() anywhere. And there are three separate yield return lines. The compiler turns all of this into a tiny machine that remembers which line ran last.

The big idea: pause and resume

The heart of yield return is pause and resume. When the loop asks for a value, the method runs until it hits a yield return. It hands over that value and freezes. All its local variables stay exactly as they were. When the loop asks again, the method wakes up on the very next line and keeps going.

How a foreach loop and an iterator method take turns running

This back and forth is the whole trick. The samosa seller fries one, you eat it, then you ask again. Nobody fries everything up front.

yield return versus a normal return

Many students mix these two up. Let us make the difference crystal clear with a table.

FeatureNormal returnyield return
How many times per methodEffectively once, it ends the methodMany times, once per value
What happens after it runsMethod is finishedMethod pauses and can resume
When the work runsRight when you call the methodOnly when something loops over it
Return typeAny type, like int or List<T>Must be IEnumerable, IEnumerable<T>, IEnumerator, or IEnumerator<T>
Memory for a big sequenceWhole list sits in memoryOne item at a time

The key line is "Method pauses and can resume". A normal return slams the door shut. A yield return leaves the door open with a bookmark on the page.

Deferred execution: nothing runs until you ask

This part surprises a lot of people, so go slowly. When you call an iterator method, the code inside does not run yet. Not a single line. You only get back a small object that knows how to produce values later.

Look at this example. The Console.WriteLine("Starting...") does not print when you might expect.

public static IEnumerable<int> CountWithMessage()
{
    Console.WriteLine("Starting...");   // does NOT run on the call line
    for (int i = 1; i <= 3; i++)
    {
        Console.WriteLine($"About to yield {i}");
        yield return i;
    }
}
 
var sequence = CountWithMessage();   // nothing prints here!
Console.WriteLine("I called the method.");
 
foreach (int n in sequence)          // NOW the method body starts
{
    Console.WriteLine($"Got {n}");
}

The output is:

I called the method.
Starting...
About to yield 1
Got 1
About to yield 2
Got 2
About to yield 3
Got 3

See how "I called the method." prints before "Starting..."? The body waited until the foreach pulled the first value. This is called deferred execution or lazy evaluation.

When does the iterator body actually run?

Call method
Get iterator
Start foreach
Body runs

Steps

1

Call method

No body code runs yet

2

Get iterator

A small state object is returned

3

Start foreach

First MoveNext request fires

4

Body runs

Code runs up to first yield

The call only creates the iterator. Looping is what drives the work.

How the compiler builds a hidden state machine

You write a simple method, but the compiler is doing heavy lifting. It rewrites your iterator into a hidden class that implements IEnumerable<T> and IEnumerator<T>. This class is a state machine. It stores a number that says "which step am I on" plus all your local variables.

When the loop calls MoveNext(), the state machine looks at its stored step, jumps to the right place, runs until the next yield return, saves the new step, and returns true. When the method ends or hits yield break, MoveNext() returns false and the loop stops.

The hidden state machine the compiler generates for an iterator

You never see this class in your own code, but it is there in the compiled output. This is why a debugger sometimes shows odd generated names when you step into an iterator.

What a foreach really does under the hood

A foreach loop is friendly sugar. Under the hood it calls three things: GetEnumerator() to get the state machine, MoveNext() to advance, and Current to read the value. The flow below shows the real machinery.

What foreach does step by step

GetEnumerator
MoveNext
Read Current
Repeat or stop

Steps

1

GetEnumerator

Ask for the iterator

2

MoveNext

Advance to next yield

3

Read Current

Use the produced value

4

Repeat or stop

Loop until MoveNext is false

foreach is a polite wrapper around GetEnumerator, MoveNext, and Current.

Knowing this helps you understand why an iterator can pause. Each MoveNext() is one "fry me a samosa" request.

Stopping early with yield break

Sometimes you want to stop producing values before the natural end of the method. The yield break statement does this. It tells the loop "I am done, there is nothing more."

public static IEnumerable<int> NumbersUntilNegative(int[] input)
{
    foreach (int value in input)
    {
        if (value < 0)
        {
            yield break;   // stop the whole sequence right here
        }
        yield return value;
    }
}
 
// {5, 8, -1, 9} produces 5 and 8, then stops at -1.

This is handy when you read data and want to stop the moment you see a marker, an error, or a sentinel value. The work after yield break never runs, which saves time.

A real-life example: reading a huge file line by line

Here is where yield return truly shines. Imagine a log file with ten million lines. If you load it all into a List<string>, your program may run out of memory. With an iterator, you read and hand out one line at a time.

public static IEnumerable<string> ReadLines(string path)
{
    using StreamReader reader = new(path);
    string? line;
    while ((line = reader.ReadLine()) != null)
    {
        yield return line;   // one line, then pause
    }
}
 
// The caller can stop early without reading the rest of the file:
foreach (string line in ReadLines("huge.log"))
{
    if (line.Contains("ERROR"))
    {
        Console.WriteLine(line);
        break;   // stop early — the file is not fully read
    }
}

Two beautiful things happen here. First, memory stays tiny because only one line is in hand at a time. Second, the break stops reading the moment you find what you want. The using block still closes the file correctly because the compiler runs your cleanup when the loop ends or is disposed.

Lazy pipelines with LINQ

yield return is the engine behind LINQ. Methods like Where, Select, and Take are all lazy iterators. They chain together and pull values one at a time. This means you can build a pipeline over a massive source and only do the work for the items you actually use.

A lazy LINQ-style pipeline pulling one value at a time

Because each stage is lazy, Take(5) means the source only ever produces five matching items, even if it could produce millions. The pull from the end controls how much work happens at the start.

Common mistakes and how to avoid them

yield return is powerful, but a few traps catch beginners. This table lists the most common ones.

MistakeWhy it hurtsFix
Iterating the same sequence twiceThe whole method runs again each timeCall ToList() once and reuse the list
Expecting exceptions on the call lineErrors only fire during iterationValidate arguments in a separate non-iterator method
Using an index like result[2]Iterators have no random accessMaterialize with ToArray() first
Keeping a DB connection open across a slow callerThe connection stays open during the whole loopRead into memory, then close the connection
Mixing yield return and return value;The compiler does not allow both in one methodUse only yield return and yield break

The double-enumeration trap is the most common. Because the method re-runs on every loop, an expensive iterator can quietly run twice and double your cost. When in doubt, materialize once.

Validating arguments the right way

Since the body is deferred, a guard clause inside an iterator does not run until iteration starts. That can hide bugs. The clean pattern is to split the method: a normal method checks arguments and immediately calls a private iterator.

public static IEnumerable<int> Range(int start, int count)
{
    if (count < 0)
        throw new ArgumentOutOfRangeException(nameof(count));
 
    return RangeIterator(start, count);   // runs the check eagerly
}
 
private static IEnumerable<int> RangeIterator(int start, int count)
{
    for (int i = 0; i < count; i++)
        yield return start + i;
}

Now a bad count throws right away on the call, the way callers expect, while the actual sequence stays lazy.

When to use yield return, and when not to

Reach for yield return when the data is large or streamed, when the caller may stop early, when you want a clean iterator without boilerplate, or when you are building a process-as-you-go pipeline. Skip it when you need the full set many times, need indexing, or must control a resource's lifetime tightly. In those cases, build a real List<T> or array once.

A good rule of thumb: produce lazily, but consume with care. If you will touch the data more than once, materialize it.

Quick recap

  • yield return hands back one value and pauses the method, then resumes on the next request.
  • A method with yield return is an iterator method and must return IEnumerable, IEnumerable<T>, IEnumerator, or IEnumerator<T>.
  • Execution is deferred (lazy): the body runs only when something loops over the result.
  • The compiler builds a hidden state machine that remembers your step and local variables.
  • yield break stops the sequence early; you cannot mix yield return with a plain return value;.
  • It saves memory for huge or streamed data and lets callers stop early.
  • Watch out for double enumeration and deferred argument checks; materialize with ToList() or ToArray() when needed.

References and further reading

Related Posts