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:

  • CancellationTokenSource is the remote control. It has the Cancel button.
  • CancellationToken is 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

  1. CancellationToken and CancellationTokenSource.
  2. How to request cancellation, and how work cooperates with it.
  3. How to pass tokens through your methods.
  4. How to cancel automatically after a timeout.
  5. What ConfigureAwait does, and why blocking calls are dangerous.
  6. Async performance tips and exception handling.
  7. Async streams: IAsyncEnumerable<T>, async iterators, and await foreach.
  8. How to produce, consume, and cancel async streams.
  9. 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?

MemberWhat it does
IsCancellationRequestedtrue once cancellation has been requested
ThrowIfCancellationRequested()Throws OperationCanceledException if cancellation was requested
CancellationToken.NoneA 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.

MemberWhat it does
TokenThe token to hand to the work
Cancel()Requests cancellation now
CancelAfter(time)Requests cancellation automatically after a delay
IsCancellationRequestedWhether 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:

WayUse 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 = default the last parameter.
  • Pass the token on to every async method you call that accepts one.
  • Never create a new CancellationTokenSource in 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 taskawait task.ConfigureAwait(false)
Resumes onThe original context (e.g. the UI thread)Any thread pool thread
Safe to touch UI after the awaitYesNo
Best forApp code: UI, pages, controllersLibrary code that never touches UI

Rules of thumb:

  • Library code (NuGet packages, shared helpers): use ConfigureAwait(false) on every await. 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 callAsync replacement
task.Resultawait 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:

TipWhy
Don't make a method async just to return another taskTask<int> GetAsync() => _repo.GetAsync(); skips a state machine
Return Task.FromResult or Task.CompletedTask for results you already haveNo waiting, no extra work
Use ValueTask for hot methods that usually finish at onceAvoids an object per call
Start independent work togetherThe biggest win: overlapping waits
Use ConfigureAwait(false) in librariesSkips switching back to the original context
Don't use async for CPU-only workIt adds overhead without freeing anything
Cancel work that is no longer neededSaves 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 itemReady immediatelyMay need to be awaited
Produced withyield returnyield return in an async method
Consumed withforeachawait foreach
Good forLists in memoryData 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:

  1. is marked async,
  2. returns IAsyncEnumerable<T>,
  3. uses yield return to 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:

  1. The iterator takes a parameter marked [EnumeratorCancellation].
  2. 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 availableAfter everything has loadedAs soon as it has loaded
MemoryWhole list in memoryOne item at a time
Stop earlyEverything was already loadedStops the producer too
Can be read twice, counted, indexedYesNo, read it once with await foreach
Endless data (feeds, chats)ImpossibleWorks
Best forSmall results you need all at onceLarge, 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

MistakeWrongCorrectWhy
Not passing the token onTask.Delay(5000)Task.Delay(5000, token)The inner call never hears the cancel
Never checking the tokenLoop with no checktoken.ThrowIfCancellationRequested();Cancellation is cooperative
Treating cancel as an errorcatch (Exception) logs itcatch (OperationCanceledException) firstThe user asked to stop; it is not a failure
Not disposing the sourcevar cts = new ...;using var cts = new ...;The source holds a timer
Blocking on async codeLoadAsync().Resultawait LoadAsync()Can deadlock UI apps; fails in the browser
ConfigureAwait(false) before UI code.ConfigureAwait(false) then label.Text = ...Plain await in UI codeYou may not be on the UI thread
Iterator token not markedCancellationToken token[EnumeratorCancellation] CancellationToken tokenOtherwise the token never reaches the iterator
foreach on an async streamforeach (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 CancellationToken as the last parameter, all the way down.
  • CancelAfter and new CancellationTokenSource(time) give timeouts that really stop the work.
  • Catch OperationCanceledException separately: cancelled is not failed.
  • ConfigureAwait(false) belongs in library code that never touches the UI.
  • Never block with .Result, .Wait() or Thread.Sleep; await instead.
  • IAsyncEnumerable<T> streams values one at a time; produce them with async + yield return, read them with await 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