Back to Blog
C#

C# Task Cancellation: Using CancellationToken Correctly

c# task cancellation: Implement cooperative cancellation in C# tasks with CancellationTokenSource, handle OperationCanceledException, and avoid common pitfalls.

CancellationTokenasync-awaitTask.NETconcurrency
Illustration of a C# task cancellation flow showing a CancellationTokenSource signaling a running task to stop.

C# task cancellation is cooperative. A CancellationTokenSource does not force a running task to stop; it only signals that cancellation has been requested, and the code inside the task must observe that signal and exit cleanly. This design avoids the problems of forcibly terminating threads, such as corrupted state or leaked resources.

Creating a CancellationTokenSource and Passing the Token

The starting point is CancellationTokenSource. It exposes a Token property, and you pass that token into the task or async method that should be cancellable.

using var cts = new CancellationTokenSource(); Task work = DoWorkAsync(cts.Token);

The token itself is a struct that carries the cancellation state. It is safe to pass the same token to multiple tasks. When you call cts.Cancel(), every task that received that token observes the cancellation on its next check.

A CancellationTokenSource should be disposed when you are done with it. It holds a timer when used with CancelAfter, and it maintains registered callbacks. Disposing releases those resources. The using declaration shown above is the simplest way to scope it correctly.

Cooperating with Cancellation Inside a Task

The code inside the task must decide where and how often to check for cancellation. The two main mechanisms are IsCancellationRequested and ThrowIfCancellationRequested.

async Task DoWorkAsync(CancellationToken token) { for (int i = 0; i < 1000; i++) { token.ThrowIfCancellationRequested(); await Task.Delay(10, token); } }

ThrowIfCancellationRequested throws OperationCanceledException when the token is cancelled. That exception is the standard way to signal cancellation to the caller. The caller can then treat it as a normal completion path rather than a failure.

The frequency of checks matters. If a loop iteration is expensive and runs for a long time, checking once per iteration may be too late. In that case, check inside the iteration at safe points where the work can be abandoned without leaving state inconsistent.

Passing the Token to Framework Methods

Many .NET APIs accept a CancellationToken. Task.Delay, HttpClient methods, file I/O, and database operations all support it. Passing the token to these methods means the underlying operation is cancelled promptly without you having to poll.

await using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, useAsync: true); byte[] buffer = new byte[4096]; int read = await stream.ReadAsync(buffer, token);

When you pass a token to a framework method, that method registers its own callback on the token. When cancellation occurs, the framework method throws OperationCanceledException. You do not need to check IsCancellationRequested before every call, but you do need to handle the exception that the framework method throws.

Registering a Callback for Non-Awaitable Work

Some operations cannot accept a token directly. For those, CancellationToken.Register lets you attach a callback that runs when cancellation is requested. This is useful for cleaning up unmanaged resources, aborting a blocking operation, or releasing a lock.

using var registration = token.Register(() => blockingQueue.CompleteAdding());

The callback runs synchronously on the thread that calls Cancel, unless you pass a synchronization context option. Keep the callback short. Long-running callbacks delay Cancel from returning and can block the cancelling thread.

The registration is an IDisposable. Disposing it before cancellation occurs removes the callback, which prevents it from running later when it is no longer relevant.

Handling OperationCanceledException

When a task is cancelled, the exception that propagates is OperationCanceledException. In the Task Parallel Library, this exception is often wrapped as TaskCanceledException, which derives from OperationCanceledException.

try { await DoWorkAsync(token); } catch (OperationCanceledException) when (token.IsCancellationRequested) { // Cancellation was requested by the caller. }

The filter checks that the cancellation actually came from the token you passed. This matters because a framework method can throw OperationCanceledException for other reasons, such as a timeout or a different token. Catching only the cancellation you initiated prevents you from swallowing unrelated errors.

When you catch cancellation, you should not rethrow it unless you need to propagate it further. If you are at the top level, the cancellation is the intended outcome, and the caller already knows how to handle it.

Timeout-Based Cancellation with CancelAfter

CancellationTokenSource has a CancelAfter method that requests cancellation after a specified delay. This is the standard way to implement timeouts without you having to manage a timer manually.

using var cts = new CancellationTokenSource(); cts.CancelAfter(TimeSpan.FromSeconds(5));

After the delay elapses, Cancel is called automatically. The same rules apply: the task observes the token and throws OperationCanceledException. CancelAfter can be called again to change the timeout, and it resets the timer each time.

This pattern is useful for HTTP requests, database queries, and any operation that should not run indefinitely. The token is passed to the framework method, and the timeout is enforced by the cancellation mechanism.

Linking Multiple Cancellation Sources

Sometimes you need to cancel a task when either one of several conditions occurs. CancellationTokenSource.CreateLinkedTokenSource combines two or more tokens into one.

using var timeoutCts = new CancellationTokenSource(); using var userCts = new CancellationTokenSource(); using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(timeoutCts.Token, userCts.Token);

The linked source is cancelled when any of its input tokens is cancelled. This is useful when a user can cancel an operation manually while a timeout also applies. The linked token is what you pass to the task.

Dispose the linked source when you are done. It holds registrations on the input tokens, and failing to dispose it can keep those registrations alive longer than necessary.

Common Mistakes and Edge Cases

One frequent mistake is to catch OperationCanceledException without checking the token. If the exception came from a different source, you may hide a real failure. The filter shown earlier is the correct guard.

Another mistake is to ignore the token entirely and rely on the framework method to cancel. If the method does not accept a token, or if the loop does not pass the token to any framework call, cancellation never happens. The task runs to completion even though the caller requested cancellation.

A third mistake is to dispose the CancellationTokenSource before the task has finished observing cancellation. Disposing cancels the token and releases resources, but it does not wait for the task to complete. If you dispose the source while the task is still running, the task may throw ObjectDisposedException when it tries to access the token. Scope the source so it outlives the task.

Finally, remember that cancellation is cooperative. A task that never checks the token and never calls a cancellable framework method will run to completion. There is no way to force it to stop without risking corrupted state. The design is intentional: the developer controls exactly when and where the work can be abandoned safely.

c# task cancellation: Practical Usage and Code Examples | RYUSLOG DEV