Cancellation
By the Syncfusion DevTutorials team · Last updated
Stop async work with CancellationToken and timeouts, use ConfigureAwait and avoid blocking, and stream data with IAsyncEnumerable and await foreach.
Introduction to Cancellation
Imagine you order a taxi using your phone.
You book a ride, but then a friend offers to drive you. You press Cancel. The app tells the driver, and the driver stops coming. No one keeps driving toward you just because the trip was already started.
Async work needs the same kind of button. If a user closes a page, types new text in a search box, or a request takes too long, the old work is no longer needed and should stop instead of wasting time, memory, and network.
In simple words: cancellation is a polite way to tell running async work, "you can stop now".
C# uses two types for this:
CancellationTokenSourceis the remote control. It has the Cancel button.CancellationTokenis the signal the work listens to.
The second half of this lesson covers async streams, which let you receive many values one at a time as they arrive, and which can be cancelled the same way.
Easy Way to Remember: the source sends the signal. The token carries it. The work checks it.
What You Will Learn
CancellationTokenandCancellationTokenSource.- How to request cancellation, and how work cooperates with it.
- How to pass tokens through your methods.
- How to cancel automatically after a timeout.
- What
ConfigureAwaitdoes, and why blocking calls are dangerous. - Async performance tips and exception handling.
- Async streams:
IAsyncEnumerable<T>, async iterators, andawait foreach. - How to produce, consume, and cancel async streams.
- When to use an async stream instead of
Task<List<T>>.
About the Run buttons in this lesson. The examples run in your browser on .NET in WebAssembly. Task.Delay stands in for slow work such as a web request. Code that needs real background threads or blocking waits is shown as plain code.
Start With a Working Example
Press Run.
Cancel a download half way
using System;
using System.Threading;
using System.Threading.Tasks;
using var cts = new CancellationTokenSource();
Task download = DownloadAsync("movie.mp4", cts.Token);
await Task.Delay(350); // the user waits a little...
Console.WriteLine("User pressed Cancel");
cts.Cancel(); // ...then presses Cancel
try
{
await download;
}
catch (OperationCanceledException)
{
Console.WriteLine("Download stopped");
}
static async Task DownloadAsync(string file, CancellationToken token)
{
for (int percent = 20; percent <= 100; percent += 20)
{
await Task.Delay(100, token); // stops at once if cancelled
Console.WriteLine($"{file}: {percent}%");
}
Console.WriteLine("Download complete");
}
The download got to 60%, then stopped. "Download complete" never printed.
Try This
Change await Task.Delay(350); to await Task.Delay(700);. The download now finishes before the user presses Cancel, so cancelling has nothing left to stop.
CancellationToken
A CancellationToken is a small value that answers one question: has someone asked me to stop?
| Member | What it does |
|---|---|
IsCancellationRequested | true once cancellation has been requested |
ThrowIfCancellationRequested() | Throws OperationCanceledException if cancellation was requested |
CancellationToken.None | A token that is never cancelled, for "no cancellation" |
Register(action) | Runs some code when cancellation is requested |
You do not create tokens directly. You get them from a CancellationTokenSource, and you pass them into methods.
Reading a token
using System;
using System.Threading;
using var cts = new CancellationTokenSource();
CancellationToken token = cts.Token;
Console.WriteLine($"Before: {token.IsCancellationRequested}");
token.Register(() => Console.WriteLine("Register: cancel was pressed"));
cts.Cancel();
Console.WriteLine($"After: {token.IsCancellationRequested}");
Console.WriteLine($"None can be cancelled? {CancellationToken.None.CanBeCanceled}");
CancellationTokenSource
A CancellationTokenSource creates tokens and controls them.
| Member | What it does |
|---|---|
Token | The token to hand to the work |
Cancel() | Requests cancellation now |
CancelAfter(time) | Requests cancellation automatically after a delay |
IsCancellationRequested | Whether Cancel has been called |
Dispose() | Frees the timer and other resources |
One source can hand the same token to many tasks. One Cancel() stops them all.
One Cancel stops three tasks
using System;
using System.Threading;
using System.Threading.Tasks;
using var cts = new CancellationTokenSource();
Task[] jobs =
{
WorkAsync("Job A", cts.Token),
WorkAsync("Job B", cts.Token),
WorkAsync("Job C", cts.Token),
};
await Task.Delay(150);
cts.Cancel();
foreach (Task job in jobs)
{
try { await job; }
catch (OperationCanceledException) { }
}
foreach (Task job in jobs)
Console.WriteLine($"Cancelled? {job.IsCanceled}");
static async Task WorkAsync(string name, CancellationToken token)
{
await Task.Delay(1000, token);
Console.WriteLine($"{name} finished");
}
CancellationTokenSource holds a timer when you use CancelAfter, so create it with using var cts = .... That disposes it automatically at the end of the block.
Requesting Cancellation
Calling Cancel() does not kill the work. It only raises a flag on the token. The work notices the flag and stops itself.
That is why it is called requesting cancellation. It is like raising your hand in a meeting: the speaker still decides when to stop talking.
Cancel raises a flag, the work reacts
using System;
using System.Threading;
using System.Threading.Tasks;
using var cts = new CancellationTokenSource();
Task counting = CountAsync(cts.Token);
await Task.Delay(250);
Console.WriteLine("Main: requesting cancellation");
cts.Cancel();
Console.WriteLine($"Main: flag is now {cts.IsCancellationRequested}");
await counting;
Console.WriteLine("Main: counting has stopped");
static async Task CountAsync(CancellationToken token)
{
int n = 0;
while (!token.IsCancellationRequested)
{
n++;
Console.WriteLine($"Count {n}");
await Task.Delay(100);
}
Console.WriteLine("Count: saw the flag, stopping cleanly");
}
Cooperative Cancellation
Cancellation in .NET is cooperative: the work must check the token, or nothing happens.
There are three ways to cooperate:
| Way | Use it when |
|---|---|
Pass the token to async methods: Task.Delay(ms, token), ReadAsync(buffer, token) | You call methods that accept a token. They stop on their own |
token.ThrowIfCancellationRequested() | Inside your own loops. It throws OperationCanceledException |
if (token.IsCancellationRequested) ... | You want to stop quietly and return a partial result |
The usual choice is ThrowIfCancellationRequested, because the exception tells the caller that the work was cancelled, not finished.
Checking the token inside a loop
using System;
using System.Threading;
using System.Threading.Tasks;
using var cts = new CancellationTokenSource();
cts.CancelAfter(250);
try
{
int processed = await ProcessOrdersAsync(10, cts.Token);
Console.WriteLine($"Processed all {processed}");
}
catch (OperationCanceledException)
{
Console.WriteLine("Processing was cancelled");
}
static async Task<int> ProcessOrdersAsync(int count, CancellationToken token)
{
for (int i = 1; i <= count; i++)
{
token.ThrowIfCancellationRequested(); // check before each order
Console.WriteLine($"Order {i} processed");
await Task.Delay(100);
}
return count;
}
If the loop never checked the token, it would process all ten orders, and calling Cancel would have no effect at all.
Passing Cancellation Tokens
A token should travel all the way down, from the method that owns the source to the lowest method that waits.
Follow these conventions:
- Make
CancellationToken cancellationToken = defaultthe last parameter. - Pass the token on to every async method you call that accepts one.
- Never create a new
CancellationTokenSourcein the middle just to ignore the caller's token.
The token travels through three layers
using System;
using System.Threading;
using System.Threading.Tasks;
using var cts = new CancellationTokenSource();
cts.CancelAfter(250);
try
{
await PlaceOrderAsync(cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("The order was cancelled");
}
static async Task PlaceOrderAsync(CancellationToken cancellationToken = default)
{
await CheckStockAsync(cancellationToken);
await ChargeCardAsync(cancellationToken);
Console.WriteLine("Order placed");
}
static async Task CheckStockAsync(CancellationToken cancellationToken = default)
{
await Task.Delay(100, cancellationToken);
Console.WriteLine("Stock checked");
}
static async Task ChargeCardAsync(CancellationToken cancellationToken = default)
{
await Task.Delay(300, cancellationToken);
Console.WriteLine("Card charged");
}
The cancel arrives while the card is being charged. Because the token was passed all the way down, Task.Delay inside ChargeCardAsync stops at once.
You can also link two tokens, so the work stops when either one is cancelled, for example the user's Cancel button or a timeout:
Stop on whichever comes first
using System;
using System.Threading;
using System.Threading.Tasks;
using var userCancel = new CancellationTokenSource();
using var timeout = new CancellationTokenSource(TimeSpan.FromMilliseconds(200));
using var linked = CancellationTokenSource.CreateLinkedTokenSource(userCancel.Token, timeout.Token);
try
{
await Task.Delay(1000, linked.Token);
}
catch (OperationCanceledException)
{
string reason = timeout.IsCancellationRequested ? "timeout" : "user";
Console.WriteLine($"Stopped by: {reason}");
}
Timeout-Based Cancellation
A timeout is just a cancellation that fires on a timer. There are two ways to set it:
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2)); // in the constructor
cts.CancelAfter(TimeSpan.FromSeconds(2)); // or later
Unlike the timeouts in lesson 2, this one really stops the work, because the work is listening to the token.
Give up on a slow search after 300 ms
using System;
using System.Threading;
using System.Threading.Tasks;
foreach (int serverMs in new[] { 100, 800 })
{
using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(300));
try
{
string result = await SearchAsync("laptop", serverMs, cts.Token);
Console.WriteLine($"Server took {serverMs} ms: {result}");
}
catch (OperationCanceledException)
{
Console.WriteLine($"Server took {serverMs} ms: cancelled after 300 ms");
}
}
static async Task<string> SearchAsync(string text, int ms, CancellationToken token)
{
await Task.Delay(ms, token);
return $"3 results for '{text}'";
}
HttpClient has its own Timeout property (100 seconds by default), and every HttpClient method also accepts a token for a shorter, per-request limit:
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
string json = await client.GetStringAsync(url, cts.Token);
ConfigureAwait
When an await finishes, the method continues. But on which thread?
In apps with a UI (WPF, WinForms, MAUI), the code after await normally comes back to the UI thread, so it can safely update the screen. This "remember where I was" information is called the synchronization context.
ConfigureAwait(false) says: "I do not need to come back to the original thread. Any thread will do."
// In a library method: no UI work after the await
string text = await File.ReadAllTextAsync(path).ConfigureAwait(false);
return text.ToUpper();
await task | await task.ConfigureAwait(false) | |
|---|---|---|
| Resumes on | The original context (e.g. the UI thread) | Any thread pool thread |
| Safe to touch UI after the await | Yes | No |
| Best for | App code: UI, pages, controllers | Library code that never touches UI |
Rules of thumb:
- Library code (NuGet packages, shared helpers): use
ConfigureAwait(false)on everyawait. It is a little faster, and it avoids deadlocks if someone blocks on your method. - Application code (UI event handlers, pages): use a plain
await. - ASP.NET Core and console apps have no synchronization context, so it makes no difference there.
It compiles and runs in the browser too, where the result is the same either way:
ConfigureAwait(false) changes where, not what
using System;
using System.Threading.Tasks;
string a = await LoadAsync("normal await");
string b = await LoadAsync("ConfigureAwait(false)").ConfigureAwait(false);
Console.WriteLine(a);
Console.WriteLine(b);
static async Task<string> LoadAsync(string label)
{
await Task.Delay(100).ConfigureAwait(false);
return $"Loaded with {label}";
}
Avoiding Blocking Calls
Blocking means making a thread sit and wait for an unfinished task. These calls all block:
| Blocking call | Async replacement |
|---|---|
task.Result | await task |
task.Wait() | await task |
task.GetAwaiter().GetResult() | await task |
Task.WaitAll(tasks) | await Task.WhenAll(tasks) |
Task.WaitAny(tasks) | await Task.WhenAny(tasks) |
Thread.Sleep(ms) | await Task.Delay(ms) |
Blocking is not just slow. In a UI app it can cause a deadlock:
// WPF button click: freezes forever
private void Button_Click(object sender, RoutedEventArgs e)
{
string text = LoadAsync().Result; // 1. UI thread blocks here, waiting
}
private async Task<string> LoadAsync()
{
await Task.Delay(1000); // 2. wants to resume on the UI thread...
return "done"; // 3. ...but the UI thread is blocked. Deadlock.
}
The UI thread waits for the task, and the task waits for the UI thread. Neither can move.
In this browser playground, blocking fails straight away with "Cannot wait on monitors on this runtime", so all blocking examples here are plain code. The fix is always the same: make the caller async and use await.
private async void Button_Click(object sender, RoutedEventArgs e)
{
string text = await LoadAsync(); // the UI stays responsive
}
Async Performance Considerations
Async is not free. Each async method creates a small state machine, and each unfinished task is an object. Usually this cost is tiny compared with the wait it saves. A few tips keep it small:
| Tip | Why |
|---|---|
Don't make a method async just to return another task | Task<int> GetAsync() => _repo.GetAsync(); skips a state machine |
Return Task.FromResult or Task.CompletedTask for results you already have | No waiting, no extra work |
Use ValueTask for hot methods that usually finish at once | Avoids an object per call |
| Start independent work together | The biggest win: overlapping waits |
Use ConfigureAwait(false) in libraries | Skips switching back to the original context |
Don't use async for CPU-only work | It adds overhead without freeing anything |
| Cancel work that is no longer needed | Saves network, CPU, and memory |
Pass the task through, or return a finished one
using System;
using System.Threading.Tasks;
Console.WriteLine(await GetNameAsync(1));
Console.WriteLine(await GetNameAsync(0));
// No async/await needed: just pass the task back.
static Task<string> GetNameAsync(int id) =>
id == 0
? Task.FromResult("Guest") // already known, no waiting
: LoadNameFromDbAsync(id); // the real async work
static async Task<string> LoadNameFromDbAsync(int id)
{
await Task.Delay(100);
return $"User #{id}";
}
Async Exception Handling
Cancellation adds one more kind of exception to handle: OperationCanceledException (and its child TaskCanceledException). A good async method treats it differently from real errors.
- Cancelled is usually not an error. The user asked for it. Log it quietly, or not at all.
- Real errors (network down, bad data) should be reported.
Catch the cancellation first, then the real errors:
Cancelled versus failed
using System;
using System.Threading;
using System.Threading.Tasks;
await RunAsync("Cancelled job", cancelAfterMs: 50, fail: false);
await RunAsync("Broken job", cancelAfterMs: 1000, fail: true);
await RunAsync("Good job", cancelAfterMs: 1000, fail: false);
static async Task RunAsync(string name, int cancelAfterMs, bool fail)
{
using var cts = new CancellationTokenSource(cancelAfterMs);
try
{
await Task.Delay(200, cts.Token);
if (fail) throw new InvalidOperationException("database is down");
Console.WriteLine($"{name}: finished");
}
catch (OperationCanceledException)
{
Console.WriteLine($"{name}: cancelled (not an error)");
}
catch (Exception ex)
{
Console.WriteLine($"{name}: FAILED - {ex.Message}");
}
}
A cancelled task has Status == Canceled, not Faulted. That is how IsCanceled and IsFaulted let you tell them apart without catching.
Never swallow OperationCanceledException inside a helper method and carry on as if the work finished. Let it travel up to the code that requested the cancellation. Otherwise, the caller thinks the work succeeded.
Introduction to Async Streams
So far, every async method has returned one result: a Task<int>, a Task<string>, a Task<List<Order>>.
Some data arrives piece by piece: messages from a chat server, rows from a big database query, lines from a log file, prices from a stock feed. Waiting for all of it before showing any of it is slow, and sometimes it never ends.
Async streams deliver values one at a time, as each one arrives.
Think of a TV series. Task<List<Episode>> is waiting until the whole season is released, then watching. An async stream is watching each episode the week it comes out.
In simple words: an async stream is a loop where each item may need an await to arrive.
IAsyncEnumerable
IAsyncEnumerable<T> is the async version of IEnumerable<T>.
IEnumerable<T> | IAsyncEnumerable<T> | |
|---|---|---|
| Each item | Ready immediately | May need to be awaited |
| Produced with | yield return | yield return in an async method |
| Consumed with | foreach | await foreach |
| Good for | Lists in memory | Data arriving over time |
It lives in System.Collections.Generic, so no extra package is needed.
async Iterator Methods
An async iterator is a method that:
- is marked
async, - returns
IAsyncEnumerable<T>, - uses
yield returnto hand out each value.
An async iterator that yields three numbers
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
await foreach (int n in CountSlowlyAsync(3))
{
Console.WriteLine($"Received {n}");
}
Console.WriteLine("Stream finished");
static async IAsyncEnumerable<int> CountSlowlyAsync(int max)
{
for (int i = 1; i <= max; i++)
{
await Task.Delay(100); // wait for the next value
Console.WriteLine($" produced {i}");
yield return i; // hand it to the consumer
}
}
Notice the order. Each value is produced and immediately received. The method pauses at yield return until the consumer asks for the next item.
await foreach
await foreach reads an async stream. For each item, it waits until the item arrives, runs the loop body, then asks for the next.
You can use break to stop early. The producer then stops too, because nobody asks it for more.
Stop reading after the first match
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
await foreach (string name in GetNamesAsync())
{
Console.WriteLine($"Checking {name}");
if (name.StartsWith("R"))
{
Console.WriteLine($"Found: {name}");
break; // stop the stream here
}
}
static async IAsyncEnumerable<string> GetNamesAsync()
{
string[] names = { "Asha", "Meena", "Ravi", "Kiran", "Divya" };
foreach (string name in names)
{
await Task.Delay(50);
yield return name;
}
Console.WriteLine("(never printed: the loop stopped early)");
}
Producing Async Streams
A real producer usually reads pages from somewhere slow and yields the items one by one, so the caller never has to think about pages.
Turn paged results into one smooth stream
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
await foreach (string order in GetAllOrdersAsync())
Console.WriteLine(order);
static async IAsyncEnumerable<string> GetAllOrdersAsync()
{
int page = 1;
while (true)
{
string[] items = await LoadPageAsync(page);
if (items.Length == 0) yield break; // no more pages: end the stream
foreach (string item in items)
yield return item;
page++;
}
}
static async Task<string[]> LoadPageAsync(int page)
{
await Task.Delay(100); // pretend web request for one page
Console.WriteLine($"-- loaded page {page}");
return page switch
{
1 => new[] { "Order 1", "Order 2" },
2 => new[] { "Order 3", "Order 4" },
3 => new[] { "Order 5" },
_ => Array.Empty<string>(),
};
}
yield break ends the stream. Each page is only loaded when the consumer is ready for more items.
Consuming Async Streams
The consumer works with each item as it comes. It can count, add up, filter, or stop early, without ever holding the whole list in memory.
Running total from a stream of sales
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
decimal total = 0;
int count = 0;
await foreach (decimal sale in GetSalesAsync())
{
count++;
total += sale;
Console.WriteLine($"Sale {count}: {sale,6} running total: {total,7}");
}
Console.WriteLine($"Average sale: {total / count:0.00}");
static async IAsyncEnumerable<decimal> GetSalesAsync()
{
decimal[] sales = { 250m, 120m, 980m, 45m };
foreach (decimal s in sales)
{
await Task.Delay(80);
yield return s;
}
}
The System.Linq.Async NuGet package adds LINQ methods such as Where, Select, and ToListAsync for async streams. It is not part of .NET itself, so write the loop yourself in this playground.
Cancelling Async Streams
An async stream can be cancelled with a token, just like a task. There are two parts:
- The iterator takes a parameter marked
[EnumeratorCancellation]. - The consumer passes the token with
.WithCancellation(token), or directly as an argument.
Stop a live price feed after 350 ms
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using var cts = new CancellationTokenSource(350);
try
{
await foreach (int price in GetPricesAsync().WithCancellation(cts.Token))
Console.WriteLine($"Price: {price}");
}
catch (OperationCanceledException)
{
Console.WriteLine("Feed stopped");
}
static async IAsyncEnumerable<int> GetPricesAsync(
[EnumeratorCancellation] CancellationToken token = default)
{
int price = 100;
while (true) // a feed that never ends on its own
{
await Task.Delay(100, token);
price += 5;
yield return price;
}
}
Without [EnumeratorCancellation], the token passed with .WithCancellation would never reach the iterator, and the feed would run forever. The compiler warns you about this (warning CS8425).
Async Streams vs Task<List>
Both return many items asynchronously. The difference is when the caller gets them.
All at the end, or one at a time
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
Console.WriteLine("Task<List<string>>:");
List<string> all = await GetListAsync();
foreach (string item in all)
Console.WriteLine($" use {item}");
Console.WriteLine("IAsyncEnumerable<string>:");
await foreach (string item in GetStreamAsync())
Console.WriteLine($" use {item}");
static async Task<List<string>> GetListAsync()
{
var list = new List<string>();
for (int i = 1; i <= 3; i++)
{
await Task.Delay(100);
Console.WriteLine($" loaded item {i}");
list.Add($"item {i}");
}
return list;
}
static async IAsyncEnumerable<string> GetStreamAsync()
{
for (int i = 1; i <= 3; i++)
{
await Task.Delay(100);
Console.WriteLine($" loaded item {i}");
yield return $"item {i}";
}
}
Task<List<T>> | IAsyncEnumerable<T> | |
|---|---|---|
| First item available | After everything has loaded | As soon as it has loaded |
| Memory | Whole list in memory | One item at a time |
| Stop early | Everything was already loaded | Stops the producer too |
| Can be read twice, counted, indexed | Yes | No, read it once with await foreach |
| Endless data (feeds, chats) | Impossible | Works |
| Best for | Small results you need all at once | Large, slow or endless data |
Easy Way to Remember: Task<List<T>> is a parcel: everything arrives in one box. IAsyncEnumerable<T> is a conveyor belt: items arrive one by one.
Common mistakes
| Mistake | Wrong | Correct | Why |
|---|---|---|---|
| Not passing the token on | Task.Delay(5000) | Task.Delay(5000, token) | The inner call never hears the cancel |
| Never checking the token | Loop with no check | token.ThrowIfCancellationRequested(); | Cancellation is cooperative |
| Treating cancel as an error | catch (Exception) logs it | catch (OperationCanceledException) first | The user asked to stop; it is not a failure |
| Not disposing the source | var cts = new ...; | using var cts = new ...; | The source holds a timer |
| Blocking on async code | LoadAsync().Result | await LoadAsync() | Can deadlock UI apps; fails in the browser |
| ConfigureAwait(false) before UI code | .ConfigureAwait(false) then label.Text = ... | Plain await in UI code | You may not be on the UI thread |
| Iterator token not marked | CancellationToken token | [EnumeratorCancellation] CancellationToken token | Otherwise the token never reaches the iterator |
foreach on an async stream | foreach (var x in GetAsync()) | await foreach (var x in GetAsync()) | Plain foreach does not compile |
What to Remember
- Cancellation politely asks async work to stop. A source sends the signal, a token carries it, the work checks it.
Cancel()only raises a flag. The work must cooperate by checking the token or passing it on.- Pass
CancellationTokenas the last parameter, all the way down. CancelAfterandnew CancellationTokenSource(time)give timeouts that really stop the work.- Catch
OperationCanceledExceptionseparately: cancelled is not failed. ConfigureAwait(false)belongs in library code that never touches the UI.- Never block with
.Result,.Wait()orThread.Sleep;awaitinstead. IAsyncEnumerable<T>streams values one at a time; produce them withasync+yield return, read them withawait foreach.- Cancel streams with
[EnumeratorCancellation]and.WithCancellation(token). - Use
Task<List<T>>for small results needed all at once, and async streams for large, slow, or endless data.
Next: Threading