Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/navigate/advanced-programming/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ items:
href: ../../standard/asynchronous-programming-patterns/executioncontext-synchronizationcontext.md
- name: SynchronizationContext and console apps
href: ../../standard/asynchronous-programming-patterns/synchronizationcontext-console-apps.md
- name: Cancel non-cancelable async operations
href: ../../standard/asynchronous-programming-patterns/cancel-non-cancelable-async-operations.md
- name: Coalesce cancellation tokens from timeouts
href: ../../standard/asynchronous-programming-patterns/coalesce-cancellation-tokens-from-timeouts.md
- name: Best practices and troubleshooting
items:
- name: Common async/await bugs
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
---
title: "Cancel non-cancelable async operations"
description: Learn practical patterns for cancellation when an async operation doesn't accept a cancellation token, including canceling the wait, canceling the operation, or both.
ms.date: 04/13/2026
ai-usage: ai-assisted
dev_langs:
- "csharp"
- "vb"
helpviewer_keywords:
- "cancellation, async"
- "Task.WhenAny"
- "WithCancellation"
- "CancellationToken"
- "NetworkStream, cancellation"
---
# Cancel non-cancelable async operations

Sometimes you need cancellation, but the operation you're waiting on doesn't accept a <xref:System.Threading.CancellationToken>. In that case, choose the behavior you need:

- Cancel the operation itself.
- Cancel only your wait.
- Cancel both the operation and your wait.

The right choice depends on who owns the operation and what cleanup guarantees you need.

## Understand the three cancellation meanings

When people say, "cancel this async call," they usually mean one of three different things:

- **Cancel the operation.** Signal the operation to stop work.
- **Cancel the wait.** Stop awaiting and continue your workflow, even if the operation still runs.
- **Cancel both.** Request operation cancellation and also stop waiting promptly.

Treat these meanings as separate design decisions. If you mix them, cancellation behavior becomes hard to reason about.

## Prefer token-aware APIs when available

Before you add a wrapper, check whether the API already supports cancellation tokens. Modern .NET APIs have much broader token support than older .NET Framework code. For example, many stream APIs in .NET now support cancellation, and `NetworkStream` async operations honor cancellation tokens.

Use token overloads whenever they exist:

:::code language="csharp" source="./snippets/cancel-non-cancelable-async-operations/csharp/Program.cs" id="PreferTokenAwareApis":::
:::code language="vb" source="./snippets/cancel-non-cancelable-async-operations/vb/Program.vb" id="PreferTokenAwareApis":::

## Cancel only the wait by using `Task.WhenAny`

When an operation doesn't accept a token, cancel your wait by racing the operation against a token-backed task. This pattern often appears as a `WithCancellation` helper:

:::code language="csharp" source="./snippets/cancel-non-cancelable-async-operations/csharp/Program.cs" id="WithCancellation":::
:::code language="vb" source="./snippets/cancel-non-cancelable-async-operations/vb/Program.vb" id="WithCancellation":::

This pattern uses <xref:System.Threading.Tasks.Task.WhenAny*> to return as soon as either task completes.

Use this approach only when it's safe for the original operation to continue in the background.

## Cancel both operation and wait when you own the operation

If you own the operation and it accepts a token, pass the token and still use a cancelable wait when needed:

:::code language="csharp" source="./snippets/cancel-non-cancelable-async-operations/csharp/Program.cs" id="CancelBoth":::
:::code language="vb" source="./snippets/cancel-non-cancelable-async-operations/vb/Program.vb" id="CancelBoth":::

This combination gives responsive cancellation for callers and cooperative shutdown for the underlying work.

## Handle abandoned operations safely

If you cancel only the wait, the original task might later fault. Keep a reference so you can observe completion and log exceptions. Otherwise, you can miss failures and make troubleshooting harder.

:::code language="csharp" source="./snippets/cancel-non-cancelable-async-operations/csharp/Program.cs" id="ObserveLateFault":::
:::code language="vb" source="./snippets/cancel-non-cancelable-async-operations/vb/Program.vb" id="ObserveLateFault":::

## See also

- [Consume the task-based asynchronous pattern](consuming-the-task-based-asynchronous-pattern.md)
- [Task cancellation](../parallel-programming/task-cancellation.md)
- [Cancellation in managed threads](../threading/cancellation-in-managed-threads.md)
- <xref:System.Threading.CancellationToken>
- <xref:System.Threading.Tasks.Task.WhenAny*>
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
---
title: "Coalesce cancellation tokens from timeouts"
description: Learn how to combine caller-driven cancellation with timeout-driven cancellation by using linked cancellation token sources in modern .NET.
ms.date: 04/13/2026
ai-usage: ai-assisted
dev_langs:
- "csharp"
- "vb"
helpviewer_keywords:
- "CancellationTokenSource.CreateLinkedTokenSource"
- "timeout, cancellation"
- "linked cancellation token"
- "CancelAfter"
- "WaitAsync"
---
# Coalesce cancellation tokens from timeouts

Many APIs need two cancellation triggers:

- A caller-provided <xref:System.Threading.CancellationToken>.
- A timeout enforced by your component.

Use <xref:System.Threading.CancellationTokenSource.CreateLinkedTokenSource*> to merge both into a single token that your async work can consume.

## Use a linked token source for caller token plus timeout

Create a timeout <xref:System.Threading.CancellationTokenSource>, link it with the caller token, and pass the linked token to the operation:

:::code language="csharp" source="./snippets/coalescing-cancellation-tokens-from-timeouts/csharp/Program.cs" id="LinkedTokenBasic":::
:::code language="vb" source="./snippets/coalescing-cancellation-tokens-from-timeouts/vb/Program.vb" id="LinkedTokenBasic":::

Dispose both `CancellationTokenSource` instances after the operation completes so registrations and timers are released promptly.

## Encapsulate linking in a helper

Wrap linking logic in a helper method when several call sites need the same policy:

:::code language="csharp" source="./snippets/coalescing-cancellation-tokens-from-timeouts/csharp/Program.cs" id="LinkedTokenHelper":::
:::code language="vb" source="./snippets/coalescing-cancellation-tokens-from-timeouts/vb/Program.vb" id="LinkedTokenHelper":::

A helper keeps timeout behavior consistent and avoids duplicate disposal bugs.

## Choose the right timeout model

Linked tokens are useful when the callee accepts a token and you need one unified cancellation path. In modern .NET, you also have these alternatives:

- Use <xref:System.Threading.CancellationTokenSource.CancelAfter*> to enforce timeouts on a `CancellationTokenSource` you own.
- Use <xref:System.Threading.Tasks.Task.WaitAsync*> when you want to time out the wait without canceling underlying work.

This example shows a `WaitAsync` timeout for "cancel wait only" semantics:

:::code language="csharp" source="./snippets/coalescing-cancellation-tokens-from-timeouts/csharp/Program.cs" id="WaitAsyncAlternative":::
:::code language="vb" source="./snippets/coalescing-cancellation-tokens-from-timeouts/vb/Program.vb" id="WaitAsyncAlternative":::

## Summary

Use linked tokens when you need to combine caller intent and infrastructure timeouts into one cancellation contract. Keep ownership and disposal rules explicit:

- If your component creates a `CancellationTokenSource`, your component disposes it.
- If callers pass a token, never dispose the caller's token source.
- If an operation doesn't accept tokens, use wait-cancellation patterns instead of linked tokens.

## See also

- [Cancel non-cancelable async operations](cancel-non-cancelable-async-operations.md)
- [Task cancellation](../parallel-programming/task-cancellation.md)
- [Cancellation in managed threads](../threading/cancellation-in-managed-threads.md)
- <xref:System.Threading.CancellationTokenSource.CreateLinkedTokenSource*>
- <xref:System.Threading.CancellationTokenSource.CancelAfter*>
- <xref:System.Threading.Tasks.Task.WaitAsync*>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
using System.Net.Sockets;

// <PreferTokenAwareApis>
public static class StreamExamples
{
public static async Task<int> ReadOnceAsync(
NetworkStream stream,
byte[] buffer,
CancellationToken cancellationToken)
{
return await stream.ReadAsync(
buffer.AsMemory(0, buffer.Length),
cancellationToken);
}
}
// </PreferTokenAwareApis>

// <WithCancellation>
public static class TaskCancellationExtensions
{
public static async Task<T> WithCancellation<T>(
this Task<T> task,
CancellationToken cancellationToken)
{
if (task.IsCompleted)
return await task.ConfigureAwait(false);

var cancellationTaskSource = new TaskCompletionSource<bool>(
TaskCreationOptions.RunContinuationsAsynchronously);

using var registration = cancellationToken.Register(
static state =>
((TaskCompletionSource<bool>)state!).TrySetResult(true),
cancellationTaskSource);

Task completed = await Task.WhenAny(task, cancellationTaskSource.Task)
.ConfigureAwait(false);

if (completed != task)
throw new OperationCanceledException(cancellationToken);

return await task.ConfigureAwait(false);
}
}
// </WithCancellation>

// <CancelBoth>
public static class CancelBothDemo
{
public static async Task<string> FetchDataAsync(CancellationToken cancellationToken)
{
await Task.Delay(500, cancellationToken);
return "payload";
}

public static async Task RunAsync()
{
using var cts = new CancellationTokenSource();
cts.CancelAfter(100);

try
{
string payload = await FetchDataAsync(cts.Token)
.WithCancellation(cts.Token);
Console.WriteLine(payload);
}
catch (OperationCanceledException)
{
Console.WriteLine("Canceled operation and wait.");
}
}
}
// </CancelBoth>

// <ObserveLateFault>
public static class ObserveLateFaultDemo
{
private static async Task<int> FaultLaterAsync()
{
await Task.Delay(250);
throw new InvalidOperationException("Background operation failed.");
}

public static async Task RunAsync()
{
Task<int> operation = FaultLaterAsync();

using var cts = new CancellationTokenSource(50);

try
{
await operation.WithCancellation(cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("Stopped waiting; operation still running.");
}

_ = operation.ContinueWith(
t => Console.WriteLine($"Observed late fault: {t.Exception!.InnerException!.Message}"),
TaskContinuationOptions.OnlyOnFaulted);

await Task.Delay(300);
Comment thread
BillWagner marked this conversation as resolved.
}
}
// </ObserveLateFault>

public class Program
{
public static async Task Main()
{
Console.WriteLine("=== Cancel both operation and wait ===");
await CancelBothDemo.RunAsync();

Console.WriteLine();
Console.WriteLine("=== Cancel wait only and observe late fault ===");
await ObserveLateFaultDemo.RunAsync();

Console.WriteLine();
Console.WriteLine("Done.");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<RootNamespace>CancelNonCancelableAsyncOperations</RootNamespace>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>

</Project>
Loading
Loading