ZetCode

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.

Program.cs
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.

Program.cs
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.

Program.cs
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.

Program.cs
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.

Program.cs
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.

Program.cs
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#:

The lifetime rules are easy to see in a small example.

Program.cs
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

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all C# tutorials.