C# CancellationToken
CancellationToken is the standard .NET way to ask asynchronous or long-running code to stop early. It does not kill a thread or interrupt code by force; it carries a cooperative cancellation request that well-written methods check and respect. Cancellation matters because users close windows, HTTP clients disconnect, timeouts expire, and background work should not keep consuming resources after its result is no longer needed.
Overview: How CancellationToken Works
A CancellationToken is a small value type that represents a cancellation signal. The signal is usually controlled by a CancellationTokenSource. The source owns the mutable state: whether cancellation has been requested, which callbacks are registered, and whether a scheduled timeout should cancel it. The token is the read-only view that you pass to methods.
This separation is important. Code that starts or owns an operation creates the CancellationTokenSource and decides when to call Cancel. Code that performs the operation receives only the CancellationToken, so it can observe cancellation but cannot normally cancel the whole operation itself. That keeps ownership clear.
Cancellation in .NET is cooperative. A token changing to the canceled state does not automatically stop a loop, close a socket, or unwind a stack frame. The running code must check IsCancellationRequested, call ThrowIfCancellationRequested, or pass the token into APIs that know how to observe it, such as Task.Delay, many stream methods, and many HTTP and database APIs.
The usual async pattern is to throw OperationCanceledException when cancellation is requested. ThrowIfCancellationRequested does exactly that and associates the exception with the token. When an async method throws that cancellation exception, the returned Task completes in the canceled state. When the caller awaits it, the caller sees an OperationCanceledException and can treat cancellation differently from a real failure.
Cancellation tokens are thread-safe for observation. One thread may call Cancel while another thread checks the token. Registered callbacks run when cancellation is requested, usually synchronously as part of the Cancel call unless a particular API documents otherwise. Because callback registration and timer resources can be involved, dispose CancellationTokenSource instances you create, especially sources created for timeouts or linked cancellation.
Syntax
using System.Threading;
using System.Threading.Tasks;
using CancellationTokenSource source = new CancellationTokenSource();
CancellationToken token = source.Token;
source.Cancel();
token.ThrowIfCancellationRequested();
await SomeOperationAsync(token);
using CancellationTokenSource timed = new CancellationTokenSource(TimeSpan.FromSeconds(5));
using CancellationTokenSource linked = CancellationTokenSource.CreateLinkedTokenSource(token, timed.Token);
| Part | Meaning |
|---|---|
CancellationTokenSource |
The object that owns cancellation state and can request cancellation. |
Token |
The read-only cancellation signal passed to worker methods. |
Cancel() |
Requests cancellation and invokes registered callbacks. |
IsCancellationRequested |
Boolean check for code that wants to stop gracefully without throwing immediately. |
ThrowIfCancellationRequested() |
Throws OperationCanceledException if the token has been canceled. |
CancelAfter |
Schedules cancellation after a timeout. |
CreateLinkedTokenSource |
Creates a source that cancels when any of several tokens cancel. |
Examples
Example 1: Cancel Before Starting Work
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
using CancellationTokenSource source = new CancellationTokenSource();
source.Cancel();
try
{
await SendReceiptAsync(source.Token);
Console.WriteLine("Receipt sent");
}
catch (OperationCanceledException)
{
Console.WriteLine("Receipt was canceled");
}
}
static async Task SendReceiptAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
await Task.Delay(10, cancellationToken);
}
}
Output:
Receipt was canceled
The source is canceled before SendReceiptAsync begins. The method checks the token immediately with ThrowIfCancellationRequested, so it exits before doing any work. This is a good habit for methods that may be called with an already-canceled token.
Example 2: Stop A Loop Cooperatively
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
using CancellationTokenSource source = new CancellationTokenSource();
int processed = await ProcessItemsAsync(
source.Token,
item =>
{
if (item == 3)
{
source.Cancel();
}
});
Console.WriteLine($"Processed: {processed}");
}
static async Task<int> ProcessItemsAsync(CancellationToken cancellationToken, Action<int> afterItem)
{
int count = 0;
for (int item = 1; item <= 5; item++)
{
if (cancellationToken.IsCancellationRequested)
{
Console.WriteLine("Stopping gracefully");
return count;
}
await Task.Delay(1);
count++;
afterItem(item);
}
return count;
}
}
Output:
Stopping gracefully
Processed: 3
This method chooses graceful completion instead of throwing. The callback simulates the owner requesting cancellation after the third processed item. The worker checks IsCancellationRequested at the start of each iteration and returns how much work was finished. That can be useful when a partial result is meaningful, but it is slower to respond than passing the token into every cancelable wait.
Example 3: Pass The Token To Async APIs
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
using CancellationTokenSource source = new CancellationTokenSource();
source.Cancel();
try
{
await LoadPageAsync(source.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("Load canceled by token");
}
}
static async Task LoadPageAsync(CancellationToken cancellationToken)
{
Console.WriteLine("Starting load");
await Task.Delay(1000, cancellationToken);
Console.WriteLine("Finished load");
}
}
Output:
Starting load
Load canceled by token
Task.Delay has an overload that accepts a CancellationToken. Because the token is already canceled, the delay completes as canceled instead of waiting. Real I/O APIs use the same pattern: pass the token down so the lowest layer can stop waiting, release resources, and complete the task promptly.
Example 4: Combine User Cancellation With A Timeout
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
using CancellationTokenSource userSource = new CancellationTokenSource();
using CancellationTokenSource timeoutSource = new CancellationTokenSource();
using CancellationTokenSource linkedSource = CancellationTokenSource.CreateLinkedTokenSource(
userSource.Token,
timeoutSource.Token);
timeoutSource.Cancel();
try
{
await SaveOrderAsync(linkedSource.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("Save stopped");
Console.WriteLine($"User canceled: {userSource.IsCancellationRequested}");
Console.WriteLine($"Timeout canceled: {timeoutSource.IsCancellationRequested}");
}
}
static async Task SaveOrderAsync(CancellationToken cancellationToken)
{
await Task.Delay(10, cancellationToken);
Console.WriteLine("Order saved");
}
}
Output:
Save stopped
User canceled: False
Timeout canceled: True
A linked token source is canceled when any of its input tokens is canceled. This is common in servers: one token may mean the HTTP client disconnected, while another token represents an application timeout. The worker method receives one token, but the caller can still inspect the original sources to understand why cancellation happened.
How It Works Step By Step
- The owner creates a
CancellationTokenSourceand passes itsTokento one or more operations. - Worker methods store no special thread control handle. They simply receive a value that can report whether cancellation was requested.
- The owner calls
Cancel,CancelAfterfires, or a linked source notices that one of its input tokens was canceled. - The source switches permanently to the canceled state and runs registered callbacks. Any future check of the token sees cancellation.
- Cancelable APIs that received the token complete their returned tasks as canceled. Code that calls
ThrowIfCancellationRequestedthrowsOperationCanceledException. - An async method that exits by throwing the cancellation exception produces a canceled task. The caller observes it with
awaitand can catchOperationCanceledException.
A token is not reset after cancellation. Once canceled, it stays canceled forever. If you need to run a new operation, create a new CancellationTokenSource. Also remember that cancellation is not failure. A canceled request usually means the caller no longer wants the work, not that the system is broken.
Common Mistakes
Accepting A Token But Not Passing It Down
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
using CancellationTokenSource source = new CancellationTokenSource();
source.Cancel();
await SlowWorkAsync(source.Token);
}
static async Task SlowWorkAsync(CancellationToken cancellationToken)
{
await Task.Delay(10);
Console.WriteLine($"Canceled flag: {cancellationToken.IsCancellationRequested}");
Console.WriteLine("Work still waited");
}
}
Output:
Canceled flag: True
Work still waited
The method accepts a token but does not use it in the wait. The caller requested cancellation, but Task.Delay(10) had no way to observe that request.
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
using CancellationTokenSource source = new CancellationTokenSource();
source.Cancel();
try
{
await SlowWorkAsync(source.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("Stopped before waiting");
}
}
static async Task SlowWorkAsync(CancellationToken cancellationToken)
{
await Task.Delay(10, cancellationToken);
Console.WriteLine("Work finished");
}
}
Output:
Stopped before waiting
Swallowing Cancellation As A Generic Error
try
{
await DownloadAsync(cancellationToken);
}
catch (Exception ex)
{
Console.WriteLine($"Failed: {ex.Message}");
}
A broad catch (Exception) treats expected cancellation like a failure. In real applications this can create noisy logs, retries that should not happen, and confusing user messages. Catch cancellation separately, or let it flow to a caller that knows what cancellation means.
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
using CancellationTokenSource source = new CancellationTokenSource();
source.Cancel();
try
{
await DownloadAsync(source.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("Download canceled");
}
}
static async Task DownloadAsync(CancellationToken cancellationToken)
{
await Task.Delay(10, cancellationToken);
}
}
Output:
Download canceled
Best Practices
- Accept a
CancellationTokenin async methods that may wait, loop, perform I/O, or run long enough for the caller to change its mind. - Put optional tokens last in method signatures, usually as
CancellationToken cancellationToken = default. - Pass the token down to lower-level async APIs instead of only checking it at the top.
- Check cancellation before expensive work starts and at reasonable points inside long loops.
- Use
ThrowIfCancellationRequestedwhen cancellation should produce a canceled task, not a partial successful result. - Use
IsCancellationRequestedwhen graceful partial completion is part of the method’s contract. - Dispose
CancellationTokenSourceobjects you create, especially sources that use timeouts, registrations, or linking. - Do not reuse a canceled source for a new operation. Create a fresh source instead.
- Do not use cancellation tokens for ordinary business validation. Invalid input should usually be reported with validation errors or exceptions, not cancellation.
Practice Exercises
- Write
CountAsyncthat loops from 1 to 10, delays briefly in each iteration, and accepts aCancellationToken. Cancel before calling it and catchOperationCanceledException. - Create a method that returns the number of items processed before cancellation. Use
IsCancellationRequestedinstead of throwing, and print the partial count. - Build a linked token source from a user token and a timeout token. Cancel the user token and print which original source caused the linked cancellation.
Summary
CancellationTokenSourcerequests cancellation;CancellationTokenlets worker code observe it.- Cancellation is cooperative. Code must check the token or pass it to APIs that understand tokens.
ThrowIfCancellationRequestedis the standard way to complete an async task as canceled.IsCancellationRequestedis useful when a method should return a partial or graceful result.- Linked tokens combine multiple cancellation reasons into one token for worker code.
- Dispose token sources you own and create a new source for each new cancelable operation.
- Treat cancellation as a normal control path, not as an unexpected application failure.
