Overview
Cancellation is cooperative. One part of the code politely notifies the other code that it’d like it to please stop. The responding code may immediately stop, or continue until it reaches a valid stopping point, or ignore the cancellation request entirely.
Most code has the form:
async Task DoSomethingAsync(int data, CancellationToken cancellationToken)
{
var intermediateValue = await DoFirstStepAsync(data, cancellationToken);
await DoSecondStepAsync(intermediateValue, cancellationToken);
}
… where the CancellationToken is passed down to whatever APIs you call.
By convention, the CancellationToken is the last in the method signature:
async Task DoSomethingAsync(int data, CancellationToken cancellationToken = default)
{
...
}
… where the default CancellationToken is CancellationToken.None, i.e.,
a cancellation token that will never be canceled.
Unless you’re also p/Invoking APIs that take timeout parameters, taking a single
CancellationToken is sufficient to represent any kind of cancellation, e.g., a
user pressing a Cancel button, an application shutting down, a client
disconnecting from a server, a timeout, etc.
The cancellation contract has canceled code throw OperationCanceledException
when the cancellation is observed and has actually canceled some work. If the
cancellation request arrives too late, then the method returns normally without
throwing OperationCanceledException.
When using Task.Run, do not pass the CancellationToken to Task.Run because
that just cancels the scheduling of the delegate to the thread pool, and not the
delegate itself, i.e.,
async Task DoSomethingAsync(CancellationToken cancellationToken)
{
var test = await Task.Run(() =>
{
// Do something, ignoring `cancellationToken`
}, cancellationToken);
...
}
… Instead, use the cancellationToken inside the delegate.
Requesting Cancellation
In most cases, the framework you’re using provides the CancellationToken,
e.g., ASP.NET provides a CancellationToken that represents an unexpected
client disconnect. Use CancellationTokenSource when you need to provide your
own CancellationToken that can be cancelled later.
Each CancellationToken created from a CancellationTokenSource is a small
struct that refers back to its CancellationTokenSource. A
CancellationToken can only respond to cancellation request. To request a
cancellation, keep a reference to the CancellationTokenSource and request
cancellations through it.
For the common case of requesting cancellation after a timeout:
async Task DoSomethingWithTimeoutAsync()
{
using CancellationTokenSource cts = new(TimeSpan.FromMinutes(5));
await DoSomethingAsync(cts.Token);
// At the end of this method, the CTS is disposed and its tokens should not
// be used after this point.
}
… or call CancelAfter on an existing CancellationTokenSource.
Consider a GUI application with a “Cancel” button:
Constructor() => CancelButton.Enabled = false;
private CancellationTokenSource? _cts;
async void StartButton_Click(...)
{
// Requirement: Either the Start or Cancel button can be enabled at any given time.
StartButton.Enabled = false;
CancelButton.Enabled = true;
using var cts = _cts = new();
try
{
await DoSomethingAsync(_cts.Token);
... // Display success in the UI.
}
catch (Exception ex)
{
... // Display error in the UI.
}
finally
{
// Requirement: Start button remain disabled until operation completes
// successfully, or with an Exception (including OperationCanceledException).
StartButton.Enabled = true;
CancelButton.Enabled = false;
}
}
async void CancelButton_Click(...)
{
if (_cts is not CancellationTokenSource cts)
throw new IllegalOperationException("Cancel called without a prior operation");
// Requirement: After cancellation, the Cancel button remains enabled but is a noop.
cts.Cancel();
}
What if the user should be able to start a new operation as soon as the old operation is cancelled, without waiting for the old operation to complete?
Constructor() => CancelButton.Enabled = false;
private CancellationTokenSource? _cts;
async void StartButton_Click(...)
{
StartButton.Enabled = false;
CancelButton.Enabled = true;
using var cts = _cts = new();
// Requirement: Only show updates when we're the current operation. Use
// `cts == _cts` because `_cts` changes every time StartButton is clicked.
try
{
await DoSomethingAsync(_cts.Token);
if (cts == _cts)
{
... // Display success in the UI.
}
catch (Exception ex)
{
if (cts == _cts)
{
... // Display error in the UI.
}
}
finally
{
StartButton.Enabled = true;
CancelButton.Enabled = false;
}
}
}
async void CancelButton_Click(...)
{
StartButton.Enabled = true; // NEW
CancelButton.Enabled = false; // NEW
if (_cts is not CancellationTokenSource cts)
throw new IllegalOperationException("Cancel called without a prior operation");
cts.Cancel();
// Requirement: Cancelled operations do not update the UI with success/errors
_cts = null; // NEW
}
Always clean up CancellationTokenSource’s resources (e.g., timeout timers,
attached listeners). This cleanup happens either on
CancellationTokenSource.Dispose() or on CancellationTokenSource.Cancel().
Ensure at least one of the two happens in a CancellationTokenSource’s
lifetime.
Detecting Cancellation
By convention, methods that take CancellationToken throw
OperationCanceledException when they are cancelled. The typical response is:
async Task TryDoSomethingAsync()
{
using CancellationTokenSource cts = new();
... // Wire up something that may cancel `cts`.
try
{
await DoThingAsync(cts.Token);
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
... // Normal error handling; logging, etc.
}
}
… because handling OperationCanceledExceptions is outside the norm.
While OperationCanceledException has a CancellationToken property, this may
not match the token from your CancellationTokenSource. If for some reason you
need to catch OperationCanceledExceptions, guard it with
cts.IsCancellationRequested and not ex.CancellationToken == cts.Token.
Responding to Cancellation via Polling
void DoSomethingAsync(CancellationToken cancellationToken)
{
while (!done)
{
cancellationToken.ThrowIfCancellationRequested();
... // Do work
}
}
void DoSomethingAntiPatternAsync(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
... // Do work
}
// Anti-pattern because we don't throw OperationCanceledException on
// cancellation. Caller can't know if the operation ran to completion.
}
How often to call ThrowIfCancellationRequested is an art. For CPU-bound code,
it’s a matter of testing what cancellation feels responsive enough. Another rule
of thumb is checking right before doing something expensive.
Responding to Cancellation via Registration
This is the common approach for asynchronous code, e.g.,
async Task DoSomethingAsync(CancellationToken cancellationToken)
{
// Always clean up your registration to avoid resource leaks.
using var registration = cancellationToken.Register(() => StopSomething());
StartSomething();
await SomethingCompletedTask;
}
Expect that your registration can (and mostly will) be invoked synchronously.
For example, any registered callbacks are invoked immediately and synchronously
by the Cancel method before it returns. Your callbacks shouldn’t perform any
blocking operations or throw exceptions.
If a callback is ever added to a CancellationToken that is already invoked,
then that callback is immediately and synchronously invoked. In the example
above, StopSomething can be called before cancellationToken.Register(() => StopSomething()) returns.
CancellationTokenSource.CancelAsync immediately transitions to the cancelled
state, and then queues the callback invocations on a thread pool thread. The
returned task completes when all callbacks have completed.
Linked Cancellation Tokens
Linked CancellationTokens are useful when you need code to be cancelled if
“A or B”. For example, if the business logic has a timeout-and-retry pattern,
while also allowing the end-user to cancel all retries with a single button
click.
Instead of doing:
async Task DoSomethingAsync(CancellationToken cancellationToken)
{
using var cts = new CancellationTokenSource();
using var registration = cancellationToken.Register(cts.Cancel);
var task = DoSomethingElseAsync(cts.Token);
... // Do something while `task` is in progress, possibly calling `cts.Cancel()`
await task;
}
… one can do:
async Task DoSomethingAsync(CancellationToken cancellationToken)
{
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
var task = DoSomethingElseAsync(cts.Token);
... // Do something while `task` is in progress, possibly calling `cts.Cancel()`
await task;
}
… where CancellationTokenSource.CreateLinkedTokenSource can take any number
of CancellationTokens, and the CancellationTokenSource will be cancelled
when any of the tokens are cancelled.
Polly makes use of linked cancellation tokens, e.g.,
async Task ExecuteWithTimeoutAsync(CancellationToken cancellationToken)
{
ResiliencePipeline pipeline = new ResiliencePipelineBuilder()
.AddTimeout(TimeSpan.FromSeconds(10))
.Build();
await pipeline.ExecuteAsync(async token =>
{
... // Code that uses `token` (not `cancellationToken`)
}, cancellationToken);
}
… token is linked to both Polly’s pipeline (cancelled after 10s) and the
outer cancellationToken that you own.
Linked cancellation tokens illustrate the folly of checking
ex.CancellationToken == cts.Token instead of cts.IsCancellationRequested. If
DoSomethingElseAsync internally uses a linked CancellationTokenSource, then
it’s possible that ex.CancellationToken != cts.Token. If you must:
async Task DoSomethingAsync(CancellationToken cancellationToken)
{
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
cts.CancelAfter(TimeSpan.FromSeconds(10));
try
{
await DoSomethingElseAsync(cts.Token);
}
catch (OperationCanceledException ex)
when (cts.IsCancellationRequested && !cancellationToken.IsCancellationRequested)
{
... // Do some recovery specific to the timeout
throw;
}
}
References
- Cancellation, Part 1: Overview. Stephen Cleary. blog.stephencleary.com . Feb 24, 2022. Accessed Sep 23, 2026.
- Cancellation, Part 2: Requesting Cancellation. Stephen Cleary. blog.stephencleary.com . Mar 3, 2022. Accessed Sep 23, 2026.
- Cancellation, Part 3: Detecting Cancellation. Stephen Cleary. blog.stephencleary.com . Mar 10, 2022. Accessed Sep 23, 2026.
- Cancellation, Part 4: Polling. Stephen Cleary. blog.stephencleary.com . Mar 17, 2022. Accessed Sep 23, 2026.
- Cancellation, Part 5: Registration. Stephen Cleary. blog.stephencleary.com . Aug 8, 2024. Accessed Sep 23, 2026.
- Cancellation, Part 6: Linking. Stephen Cleary. blog.stephencleary.com . Oct 10, 2022. Accessed Sep 23, 2026.