15 minlesson

Task Continuations

Task Continuations

Continuations allow you to specify code that runs after a task completes. While async/await is preferred for most scenarios, understanding continuations helps with advanced patterns.

ContinueWith Basics

csharp
1Task<int> task = ComputeValueAsync();
2
3Task<string> continuation = task.ContinueWith(t =>
4{
5 int result = t.Result; // Get result from completed task
6 return $"The answer is {result}";
7});
8
9string message = await continuation;

When to Use ContinueWith

Use async/awaitUse ContinueWith
Most scenariosDynamic task chaining
Clear control flowAttaching to existing tasks
Exception handlingFire-and-forget logging
Readable codeTaskCompletionSource patterns

Task States in Continuations

The continuation receives the completed (antecedent) task:

csharp
1task.ContinueWith(antecedent =>
2{
3 if (antecedent.IsCompletedSuccessfully)
4 {
5 Console.WriteLine($"Result: {antecedent.Result}");
6 }
7 else if (antecedent.IsFaulted)
8 {
9 Console.WriteLine($"Error: {antecedent.Exception?.Message}");
10 }
11 else if (antecedent.IsCanceled)
12 {
13 Console.WriteLine("Task was canceled");
14 }
15});

TaskContinuationOptions

Control when continuations run:

csharp
1// Only run if task succeeded
2task.ContinueWith(
3 t => ProcessResult(t.Result),
4 TaskContinuationOptions.OnlyOnRanToCompletion);
5
6// Only run if task failed
7task.ContinueWith(
8 t => LogError(t.Exception),
9 TaskContinuationOptions.OnlyOnFaulted);
10
11// Only run if task was canceled
12task.ContinueWith(
13 t => HandleCancellation(),
14 TaskContinuationOptions.OnlyOnCanceled);

Common Options

csharp
1// Run synchronously if possible (same thread)
2TaskContinuationOptions.ExecuteSynchronously
3
4// Run on a specific condition
5TaskContinuationOptions.OnlyOnRanToCompletion
6TaskContinuationOptions.OnlyOnFaulted
7TaskContinuationOptions.OnlyOnCanceled
8TaskContinuationOptions.NotOnRanToCompletion
9TaskContinuationOptions.NotOnFaulted
10TaskContinuationOptions.NotOnCanceled
11
12// Hint for long-running work
13TaskContinuationOptions.LongRunning

Chaining Multiple Continuations

csharp
1Task<int> pipeline = GetDataAsync()
2 .ContinueWith(t => Parse(t.Result))
3 .ContinueWith(t => Validate(t.Result))
4 .ContinueWith(t => Transform(t.Result));
5
6int result = await pipeline;

Continuation with Cancellation

csharp
1var cts = new CancellationTokenSource();
2
3task.ContinueWith(
4 t => ProcessResult(t.Result),
5 cts.Token, // Cancellation token
6 TaskContinuationOptions.OnlyOnRanToCompletion,
7 TaskScheduler.Default);
8
9// Cancel continuation (not the original task)
10cts.Cancel();

Unwrapping Nested Tasks

When continuation returns a task, use Unwrap():

csharp
1// Without Unwrap: Task<Task<string>>
2Task<Task<string>> nested = task.ContinueWith(t => FetchMoreDataAsync());
3
4// With Unwrap: Task<string>
5Task<string> unwrapped = task.ContinueWith(t => FetchMoreDataAsync()).Unwrap();
6
7string result = await unwrapped;

Fire-and-Forget Logging

A practical use of ContinueWith:

csharp
1public async Task<string> ProcessDataAsync(string input)
2{
3 var result = await DoWorkAsync(input);
4
5 // Log without awaiting (fire-and-forget)
6 _ = LogAsync(result).ContinueWith(t =>
7 {
8 if (t.IsFaulted)
9 Console.WriteLine($"Logging failed: {t.Exception?.Message}");
10 }, TaskContinuationOptions.OnlyOnFaulted);
11
12 return result;
13}

Equivalent async/await Code

Most ContinueWith code can be written more clearly with async/await:

csharp
1// ContinueWith version
2Task result = task
3 .ContinueWith(t => Step1(t.Result),
4 TaskContinuationOptions.OnlyOnRanToCompletion)
5 .ContinueWith(t => Step2(t.Result),
6 TaskContinuationOptions.OnlyOnRanToCompletion);
7
8// async/await version (preferred)
9async Task ProcessAsync()
10{
11 try
12 {
13 var r1 = await task;
14 var r2 = Step1(r1);
15 var r3 = Step2(r2);
16 }
17 catch (Exception ex)
18 {
19 // Handle errors
20 }
21}

TaskCompletionSource with Continuations

Advanced pattern for converting callbacks to Tasks:

csharp
1public Task<string> WrapCallbackApiAsync(string input)
2{
3 var tcs = new TaskCompletionSource<string>();
4
5 LegacyApi.BeginOperation(input,
6 result => tcs.SetResult(result),
7 error => tcs.SetException(new Exception(error)));
8
9 return tcs.Task;
10}

Multiple Continuations on Same Task

You can attach multiple continuations:

csharp
1Task<int> task = ComputeAsync();
2
3// All three run when task completes
4task.ContinueWith(t => LogResult(t.Result));
5task.ContinueWith(t => UpdateCache(t.Result));
6task.ContinueWith(t => NotifyClients(t.Result));
7
8await task;

Exception Handling in Continuations

csharp
1// Exception in continuation is stored in the continuation task
2Task continuation = task.ContinueWith(t =>
3{
4 throw new InvalidOperationException("Continuation failed");
5});
6
7try
8{
9 await continuation;
10}
11catch (InvalidOperationException ex)
12{
13 // Caught here
14}

Key Takeaways

  • ContinueWith specifies code to run after task completion
  • Use TaskContinuationOptions to filter by outcome
  • async/await is preferred for most scenarios
  • Continuations useful for fire-and-forget, logging, dynamic chains
  • Use Unwrap() when continuation returns a Task
  • Multiple continuations can attach to the same task