C# CancellationToken
last modified October 5, 2026
C# CancellationToken tutorial shows how to cancel asynchronous operations
cooperatively with the CancellationToken and
CancellationTokenSource types.
A CancellationToken is a cooperative cancellation signal. It is
produced by a CancellationTokenSource, and an operation that accepts
the token observes it and stops gracefully when the signal arrives. The token is
not a command that forces code to stop: cancellation is not the same as aborting
a thread. Nothing is interrupted by force, and code that simply ignores the token
keeps running to completion. Well-behaved asynchronous APIs therefore accept a
token, check it at sensible points, and throw
OperationCanceledException when cancellation is requested.
C# CancellationToken example
In the first example, a background task polls the token in a loop and returns when the main thread cancels the source.
using System.Threading;
using System.Threading.Tasks;
using var source = new CancellationTokenSource();
var token = source.Token;
Console.WriteLine($"The token can be cancelled: {token.CanBeCanceled}");
var worker = Task.Run(() =>
{
int ticks = 0;
while (true)
{
if (token.IsCancellationRequested)
{
Console.WriteLine("Worker: cancellation requested, leaving the loop");
return;
}
ticks++;
}
}, token);
await Task.Delay(100);
source.Cancel();
await worker;
Console.WriteLine($"Main: token cancelled: {token.IsCancellationRequested}");
Console.WriteLine("Main: worker finished");
The source owns the state, and the token is a lightweight view over that state.
A source that was never asked to cancel still produces a token whose
CanBeCanceled property is true; only
CancellationToken.None reports false.
using var source = new CancellationTokenSource(); var token = source.Token;
The check inside the loop is what makes the operation cancellable. Reading
IsCancellationRequested alone does not stop anything; the
return is what ends the work in an orderly way.
if (token.IsCancellationRequested)
{
...
return;
}
The ticks counter is deliberately not printed, because the number of
iterations depends on how fast the machine runs the loop. Only the state
transitions are printed, so the output is the same on every run.
$ dotnet run The token can be cancelled: True Worker: cancellation requested, leaving the loop Main: token cancelled: True Main: worker finished
Passing the token to Task.Run does not make the delegate itself
cancellable. If the token is already cancelled when the task starts, the task
moves straight to the Canceled state and the delegate never runs.
Otherwise the delegate is responsible for observing the token on its own.
C# CancellationToken and Task.Delay
Several framework methods accept a token directly. The overload
Task.Delay(int, CancellationToken) is the most common one: when the
token is cancelled, the delay stops waiting and the await throws an
OperationCanceledException.
using System.Threading;
using System.Threading.Tasks;
using var source = new CancellationTokenSource();
var token = source.Token;
source.CancelAfter(200);
try
{
Console.WriteLine("Waiting for 5 seconds ...");
await Task.Delay(5000, token);
Console.WriteLine("The delay completed");
}
catch (OperationCanceledException e)
{
Console.WriteLine($"Caught {e.GetType().Name}");
Console.WriteLine($"e.CancellationToken.IsCancellationRequested: {e.CancellationToken.IsCancellationRequested}");
Console.WriteLine($"e.CancellationToken == token: {e.CancellationToken == token}");
}
The 5 second delay never runs to its end, because the token is cancelled after 200 milliseconds and the delay gives up immediately.
await Task.Delay(5000, token);
TaskCanceledException derives from
OperationCanceledException, so a single
catch (OperationCanceledException) handles both the exceptions that
the framework throws and the ones your own code throws.
catch (OperationCanceledException e)
{
...
}
The caught exception carries the token that fired. It is the same token instance
that was passed to the delay, and its
IsCancellationRequested property is true, which lets a
caller distinguish a cancellation from a real failure.
$ dotnet run Waiting for 5 seconds ... Caught TaskCanceledException e.CancellationToken.IsCancellationRequested: True e.CancellationToken == token: True
Catching the exception here only prints a message. In a real application the usual reaction is to clean up local state and rethrow, so that the caller which requested the cancellation learns that the operation did not finish.
C# CancellationToken in a method that takes a token
Cancellation is propagated by passing the token down the call chain. The
convention is that a cancellation-aware API accepts its token as the
last parameter, and that the parameter defaults to
CancellationToken.None (written as default) so that
callers which do not care about cancellation can omit it.
using System.Threading;
using System.Threading.Tasks;
using var source = new CancellationTokenSource();
var token = source.Token;
var work = DoWorkAsync("report", token);
source.CancelAfter(250);
try
{
await work;
}
catch (OperationCanceledException)
{
Console.WriteLine("Main: the work was cancelled");
}
async Task DoWorkAsync(string name, CancellationToken token = default)
{
Console.WriteLine($"Started {name}");
await Task.Delay(100, token);
Console.WriteLine("Checkpoint 1 passed");
token.ThrowIfCancellationRequested();
await Task.Delay(100, token);
Console.WriteLine("Checkpoint 2 passed");
token.ThrowIfCancellationRequested();
await Task.Delay(300, token);
Console.WriteLine($"Finished {name}");
}
The optional parameter makes the method usable both in code that supports cancellation and in code that does not. The token travels down the call chain, so every cancellable operation inside the method receives the same signal.
async Task DoWorkAsync(string name, CancellationToken token = default)
Inside a long method, call ThrowIfCancellationRequested at
checkpoints between pieces of work. It throws
OperationCanceledException, which is the expected way for an
asynchronous method to report that it stopped early.
token.ThrowIfCancellationRequested();
Each await on a cancellable operation is itself a checkpoint, so in
this example the token is observed by the delays as well as by the explicit
calls.
$ dotnet run Started report Checkpoint 1 passed Checkpoint 2 passed Main: the work was cancelled
The first two delays finish before the cancellation arrives, so both checkpoints are printed. The third delay is still pending when the source is cancelled after 250 milliseconds, and it is that delay which throws.
C# CancellationTokenSource.CancelAfter
CancelAfter schedules cancellation for a moment in the future. It
accepts a TimeSpan or a number of milliseconds, and calling it again
resets the timer.
using System.Threading;
using System.Threading.Tasks;
using var source = new CancellationTokenSource();
var token = source.Token;
source.CancelAfter(TimeSpan.FromMilliseconds(300));
Console.WriteLine($"Cancelled before the delay: {token.IsCancellationRequested}");
try
{
Console.WriteLine("Waiting on an infinite delay ...");
await Task.Delay(Timeout.InfiniteTimeSpan, token);
}
catch (OperationCanceledException)
{
Console.WriteLine("The delay was cancelled by CancelAfter");
}
Console.WriteLine($"Cancelled after the delay: {token.IsCancellationRequested}");
using var timeoutSource = new CancellationTokenSource(TimeSpan.FromMilliseconds(300));
try
{
Console.WriteLine("Waiting again ...");
await Task.Delay(Timeout.InfiniteTimeSpan, timeoutSource.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("The second delay was cancelled by the constructor timeout");
}
The token is not cancelled yet when the first line runs, and it is cancelled by the time the infinite delay returns.
source.CancelAfter(TimeSpan.FromMilliseconds(300));
A source constructed with a timeout is equivalent to creating one and calling
CancelAfter immediately: the constructor overload
new CancellationTokenSource(TimeSpan) sets the same timer. Both
produce a token that becomes cancelled once the interval elapses.
using var timeoutSource = new CancellationTokenSource(TimeSpan.FromMilliseconds(300));
$ dotnet run Cancelled before the delay: False Waiting on an infinite delay ... The delay was cancelled by CancelAfter Cancelled after the delay: True Waiting again ... The second delay was cancelled by the constructor timeout
Pass Timeout.InfiniteTimeSpan to Task.Delay when you
want a delay that ends only through cancellation. That is the idiomatic way to
build a wait that a cancellation token can release.
C# linked cancellation tokens
CancellationTokenSource.CreateLinkedTokenSource joins several tokens
into one. The linked token becomes cancelled as soon as any of the source tokens
is cancelled, which is exactly what a method needs when it must respect both an
external token and its own timeout.
using System.Threading;
using System.Threading.Tasks;
async Task RunLinkedAsync(CancellationToken externalToken, TimeSpan timeout)
{
using var timeoutSource = new CancellationTokenSource(timeout);
using var linked = CancellationTokenSource.CreateLinkedTokenSource(
externalToken, timeoutSource.Token);
try
{
await Task.Delay(Timeout.InfiniteTimeSpan, linked.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine(" the worker was cancelled");
}
Console.WriteLine($" external: {externalToken.IsCancellationRequested}, " +
$"timeout: {timeoutSource.IsCancellationRequested}, " +
$"linked: {linked.IsCancellationRequested}");
}
using var external = new CancellationTokenSource();
external.CancelAfter(TimeSpan.FromMilliseconds(100));
Console.WriteLine("External cancellation wins:");
await RunLinkedAsync(external.Token, TimeSpan.FromSeconds(5));
using var silent = new CancellationTokenSource();
Console.WriteLine("Timeout cancellation wins:");
await RunLinkedAsync(silent.Token, TimeSpan.FromMilliseconds(100));
A CancellationTokenSource implements IDisposable, so
the linked source and the timeout source are wrapped in using
declarations. It is the source that is disposable, not the token:
CancellationToken itself has no Dispose method.
using var linked = CancellationTokenSource.CreateLinkedTokenSource(
externalToken, timeoutSource.Token);
The linked source reports its own state. In the first run only the external token is cancelled, while in the second run only the timeout fires. In both runs the linked token ends up cancelled, and the delay stops.
$ dotnet run External cancellation wins: the worker was cancelled external: True, timeout: False, linked: True Timeout cancellation wins: the worker was cancelled external: False, timeout: True, linked: True
Disposing a linked source does not cancel it, and it does not dispose the source tokens. Dispose the linked source when the operation ends, and leave the lifetime of the parent tokens to their owner; a linked source that is never disposed keeps a registration on each parent token alive.
C# CancellationToken registration
CancellationToken.Register attaches a callback that runs when the
token is cancelled. The registration handle returned by the method is itself
disposable, and disposing it unregisters the callback so that it never runs.
using System.Threading;
using System.Threading.Tasks;
using var source = new CancellationTokenSource();
var token = source.Token;
int cancellingThread = Environment.CurrentManagedThreadId;
using var first = token.Register(() =>
Console.WriteLine("First callback on the cancelling thread: " +
$"{Environment.CurrentManagedThreadId == cancellingThread}"));
var second = token.Register(state =>
Console.WriteLine($"Second callback with state: {state}"), "data");
second.Dispose();
Console.WriteLine("Cancelling ...");
source.Cancel();
Console.WriteLine("The Cancel call has returned");
Callbacks run synchronously on the thread that calls Cancel. The
Cancel call returns only after every callback has finished, and that
is why the printed message appears between the two lines written by the main
thread.
source.Cancel();
The second registration is disposed before the cancellation, so its callback is never invoked. Disposing a registration after the token has already been cancelled is harmless: it simply does nothing.
second.Dispose();
$ dotnet run Cancelling ... First callback on the cancelling thread: True The Cancel call has returned
A callback must be quick and must not throw, because an exception raised inside a
registration is aggregated and rethrown from Cancel. The overload
that takes a state object avoids a closure allocation, and
token.WaitHandle is available when blocking code needs to wait for
the cancellation instead of registering a callback.
C# CancellationToken best practices
Keep the following rules in mind when working with cancellation in C#:
- Accept a token as the last parameter of an asynchronous method and give it
the default
default(that is,CancellationToken.None) only when omission is intentional. An API that claims to support cancellation must actually observe the token. - Never ignore the token inside a long loop. Check it with
ThrowIfCancellationRequested, or pollIsCancellationRequested, so that the loop cannot run forever. OperationCanceledExceptionis the expected way to exit a cancelled operation. Do not swallow it and return a normal result as if the work had completed; catch it to clean up local state, then let it propagate.- Prefer
ThrowIfCancellationRequestedover a manualif (token.IsCancellationRequested) return;in async methods, because it produces the exception that callers expect. - Dispose a
CancellationTokenSourcewithusing. A disposed source cannot be used again:CancelthrowsObjectDisposedException. - Do not cancel a source and then reuse it. Cancellation is permanent for that
source; create a new
CancellationTokenSourcefor every operation. - Keep registrations short-lived and dispose the registration, the linked source, and the source itself when the operation ends.
The lifetime rules are easy to see in a small example.
using System.Threading;
var source = new CancellationTokenSource();
var token = source.Token;
source.Cancel();
Console.WriteLine($"First source cancelled: {token.IsCancellationRequested}");
source.Dispose();
try
{
source.Cancel();
}
catch (ObjectDisposedException e)
{
Console.WriteLine($"Cannot cancel a disposed source: {e.GetType().Name}");
}
using var next = new CancellationTokenSource();
Console.WriteLine($"A new source starts uncancelled: {!next.Token.IsCancellationRequested}");
Once disposed, the old source refuses to do anything, but the token it produced still reports its last known state, which is why an already-cancelled token remains safe to read. A fresh source, in contrast, starts with a token that is not cancelled.
$ dotnet run First source cancelled: True Cannot cancel a disposed source: ObjectDisposedException A new source starts uncancelled: True
Source
CancellationToken Struct - Microsoft Learn
CancellationTokenSource Class - Microsoft Learn
Task.Delay Method - Microsoft Learn
Cancellation in managed threads - Microsoft Learn
In this article we have worked with cancellation in C#.
Author
List all C# tutorials.