Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
Sometimes you need cancellation, but the operation you're waiting on doesn't accept a 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:
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);
}
}
Public Module StreamExamples
Public Async Function ReadOnceAsync(
stream As NetworkStream,
buffer As Byte(),
cancellationToken As CancellationToken) As Task(Of Integer)
Return Await stream.ReadAsync(
buffer.AsMemory(0, buffer.Length),
cancellationToken)
End Function
End Module
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:
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);
}
}
Public Module TaskCancellationExtensions
<Extension()>
Public Async Function WithCancellation(Of T)(
operationTask As Task(Of T),
cancellationToken As CancellationToken) As Task(Of T)
If operationTask.IsCompleted Then
Return Await operationTask
End If
Dim cancellationTaskSource =
New TaskCompletionSource(Of Boolean)(TaskCreationOptions.RunContinuationsAsynchronously)
Using registration = cancellationToken.Register(
Sub(state)
DirectCast(state, TaskCompletionSource(Of Boolean)).TrySetResult(True)
End Sub,
cancellationTaskSource)
Dim completed = Await Task.WhenAny(operationTask, cancellationTaskSource.Task)
If completed IsNot operationTask Then
Throw New OperationCanceledException(cancellationToken)
End If
End Using
Return Await operationTask
End Function
End Module
This pattern uses 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:
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.");
}
}
}
Public Module CancelBothDemo
Public Async Function FetchDataAsync(cancellationToken As CancellationToken) As Task(Of String)
Await Task.Delay(500, cancellationToken)
Return "payload"
End Function
Public Async Function RunAsync() As Task
Using cts = New CancellationTokenSource()
cts.CancelAfter(100)
Try
Dim payload = Await FetchDataAsync(cts.Token).WithCancellation(cts.Token)
Console.WriteLine(payload)
Catch ex As OperationCanceledException
Console.WriteLine("Canceled operation and wait.")
End Try
End Using
End Function
End Module
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.
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);
}
}
Public Module ObserveLateFaultDemo
Private Async Function FaultLaterAsync() As Task(Of Integer)
Await Task.Delay(250)
Throw New InvalidOperationException("Background operation failed.")
End Function
Public Async Function RunAsync() As Task
Dim operation As Task(Of Integer) = FaultLaterAsync()
Using cts = New CancellationTokenSource(50)
Try
Await operation.WithCancellation(cts.Token)
Catch ex As OperationCanceledException
Console.WriteLine("Stopped waiting; operation still running.")
End Try
End Using
Dim observed = operation.ContinueWith(
Sub(t)
Console.WriteLine($"Observed late fault: {t.Exception.InnerException.Message}")
End Sub,
TaskContinuationOptions.OnlyOnFaulted)
Await observed
End Function
End Module