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.
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.
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.
| Feature | Normal return | yield return |
|---|---|---|
| How many times per method | Effectively once, it ends the method | Many times, once per value |
| What happens after it runs | Method is finished | Method pauses and can resume |
| When the work runs | Right when you call the method | Only when something loops over it |
| Return type | Any type, like int or List<T> | Must be IEnumerable, IEnumerable<T>, IEnumerator, or IEnumerator<T> |
| Memory for a big sequence | Whole list sits in memory | One 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 3See 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?
Steps
Call method
No body code runs yet
Get iterator
A small state object is returned
Start foreach
First MoveNext request fires
Body runs
Code runs up to first yield
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.
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
Steps
GetEnumerator
Ask for the iterator
MoveNext
Advance to next yield
Read Current
Use the produced value
Repeat or stop
Loop until MoveNext is false
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.
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.
| Mistake | Why it hurts | Fix |
|---|---|---|
| Iterating the same sequence twice | The whole method runs again each time | Call ToList() once and reuse the list |
| Expecting exceptions on the call line | Errors only fire during iteration | Validate arguments in a separate non-iterator method |
Using an index like result[2] | Iterators have no random access | Materialize with ToArray() first |
| Keeping a DB connection open across a slow caller | The connection stays open during the whole loop | Read into memory, then close the connection |
Mixing yield return and return value; | The compiler does not allow both in one method | Use 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 returnhands back one value and pauses the method, then resumes on the next request.- A method with
yield returnis an iterator method and must returnIEnumerable,IEnumerable<T>,IEnumerator, orIEnumerator<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 breakstops the sequence early; you cannot mixyield returnwith a plainreturn 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()orToArray()when needed.
References and further reading
- yield statement — C# reference (Microsoft Learn)
- Iterators — C# (Microsoft Learn)
- Errors and warnings for iterator methods and yield return (Microsoft Learn)
- Iterate through collections — C# programming guide (Microsoft Learn)
- C# Yield Return Statement: A Deep Dive — Rahul Nath
Related Posts
Building High-Performance .NET Apps With C# Channels
Learn C# Channels in .NET 10 with simple examples. Pass data safely between producers and consumers and build fast, smooth, high-performance apps.
C# 15 Union Types: The Easy Guide With Real Examples
Union types in C# 15 (.NET 11) let one method safely return one of many shapes. Learn how they work with simple, real-life examples, diagrams, and a clear comparison with OneOf.
5 Awesome C# Refactoring Tips to Write Cleaner Code
Learn 5 simple C# refactoring tips with examples in modern C# 14. Make your code cleaner, safer, and easier to read using guard clauses and more.
Functional Programming in C#: The Practical Parts You Will Actually Use
A warm, beginner-friendly guide to functional programming in C#: records, immutability, pattern matching, switch expressions, pure functions, and LINQ.
How to Write Elegant Code With C# Switch Expressions
Learn C# switch expressions the easy way. Master pattern matching with type, relational, property, and list patterns using simple examples, diagrams, and tables.
The 3 C# PDF Libraries Every Developer Must Know
A friendly guide to QuestPDF, PDFsharp, and iText for C#. Learn what each does, their licensing, code examples, and how to pick the right one.