Async Streams
Vaibhav • September 11, 2025
In the previous article, we explored progress reporting using IProgress<T>
, which allowed us to provide real-time feedback during
asynchronous operations. Now we turn to a powerful feature introduced in C# 8.0 that makes working with
asynchronous sequences much more natural: async streams. This feature lets you consume data
asynchronously using await foreach
, making it ideal for scenarios where data
arrives over time-like reading from a network, processing files, or streaming events.
In this article, we’ll explore how async streams work, how to define and consume them, and how they integrate with cancellation and exception handling. We’ll also look at how they compare to traditional enumerables and tasks, and how to use them effectively in real-world applications.
What Are Async Streams?
An async stream is a method that returns an IAsyncEnumerable<T>
. Unlike a
regular IEnumerable<T>
, which returns all items immediately, an async
stream yields items one at a time as they become available. You consume them using await foreach
, which pauses until the next item is ready.
async IAsyncEnumerable<int> GenerateNumbersAsync()
{
for (int i = 0; i <= 5; i++)
{
await Task.Delay(500); // simulate delay
yield return i;
}
}
This method returns numbers from 0 to 5, one every half second. The yield return
keyword emits each value, and await
allows asynchronous work between items.
Consuming Async Streams
To consume an async stream, use await foreach
. This works just like a regular
foreach
, but it waits for each item asynchronously.
await foreach (int number in GenerateNumbersAsync())
{
Console.WriteLine($"Received: {number}");
}
This loop prints each number as it arrives. The await
ensures that the loop
doesn’t block the thread while waiting for the next item.
Async streams are ideal for scenarios where data arrives gradually-like reading lines from a file, receiving messages from a server, or processing user input.
Async Streams vs IEnumerable vs Task
Before async streams, you had two main options for returning data:
Use IEnumerable<T>
for synchronous sequences, or Task<T>
for a single asynchronous result. But what if you want to return
multiple asynchronous results? You’d have to use List<T>
with Task
, which means collecting all items before returning.
Async streams solve this by combining yield return
with await
. You can return items one at a time, asynchronously, without waiting for
the entire sequence to complete.
Real-World Example: Streaming File Lines
Let’s say you want to read a large file line by line, but you don’t want to load the entire file into memory. You can use async streams to read and yield each line as it’s read.
async IAsyncEnumerable<string> ReadLinesAsync(string path)
{
using var reader = new StreamReader(path);
while (!reader.EndOfStream)
{
string line = await reader.ReadLineAsync();
yield return line;
}
}
This method reads each line asynchronously and yields it. The caller can process each line as it arrives, without waiting for the entire file.
await foreach (string line in ReadLinesAsync("data.txt"))
{
Console.WriteLine(line);
}
This loop prints each line from the file, one at a time. It’s efficient and memory-friendly, especially for large files.
Async Streams with Cancellation
You can add cancellation support to async streams using CancellationToken
. This
lets the caller stop the stream early-useful for user-triggered cancellation or timeouts.
async IAsyncEnumerable<int> CountAsync([EnumeratorCancellation] CancellationToken token)
{
for (int i = 0; i < 100; i++)
{
token.ThrowIfCancellationRequested();
await Task.Delay(100, token);
yield return i;
}
}
The EnumeratorCancellation
attribute tells the compiler to pass the token to
the enumerator. Inside the method, check the token and throw if cancelled.
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
await foreach (int value in CountAsync(cts.Token))
{
Console.WriteLine(value);
}
This loop runs for 2 seconds, then cancels. The stream exits gracefully, and no more values are returned.
Exception Handling in Async Streams
You can use try-catch
around await foreach
to
handle exceptions thrown by the stream. This works just like any other async method.
async IAsyncEnumerable<int> FaultyStream()
{
yield return 1;
await Task.Delay(500);
throw new InvalidOperationException("Something went wrong");
}
try
{
await foreach (int value in FaultyStream())
{
Console.WriteLine(value);
}
}
catch (Exception ex)
{
Console.WriteLine("Caught: " + ex.Message);
}
This stream throws an exception after yielding one value. The try-catch
catches
it and prints the error.
Combining Async Streams with LINQ
You can use LINQ-like methods with async streams, but they’re not built into System.Linq
. Instead, use System.Linq.Async
from
the Microsoft.EntityFrameworkCore
or System.Interactive.Async
packages.
await foreach (var item in GenerateNumbersAsync().WhereAwait(async n => await Task.FromResult(n % 2 == 0)))
{
Console.WriteLine($"Even: {item}");
}
This filters the stream to only even numbers. The WhereAwait
method allows
asynchronous filtering.
Common Mistakes
One common mistake is trying to use foreach
instead of await foreach
. Async streams require await
because each item may arrive asynchronously.
Another mistake is forgetting to use EnumeratorCancellation
when passing a
cancellation token. Without it, the token won’t be passed correctly, and cancellation won’t work.
Also, don’t use yield return
inside regular async methods. It only works in
methods that return IAsyncEnumerable<T>
.
You can use await foreach
with Channel<T>
for advanced producer-consumer scenarios. Channels provide
more control over buffering and concurrency.
Summary
Async streams are a powerful feature that lets you return and consume asynchronous sequences using IAsyncEnumerable<T>
and await foreach
.
They simplify scenarios where data arrives over time and integrate naturally with async workflows. You’ve
learned how to define async streams, consume them with await foreach
, handle
cancellation and exceptions, and use them in real-world applications like file reading and data streaming.
In the next article, we’ll explore Deadlock Prevention, where we’ll learn how to avoid common pitfalls that cause async code to freeze or hang. You’ll understand how deadlocks happen, how to detect them, and how to design your code to stay responsive and safe.