C#
Task Parallel Library
asynchronous programming
ContinueWith
multithreading

C Chained ContinueWith Not Waiting for Previous Task to Complete

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

ContinueWith chains in C# can appear to run out of order when tasks are nested, not awaited, or scheduled on unexpected contexts. The issue is usually not that ContinueWith ignores order, but that code is observing the wrong task completion boundary. Understanding task unwrapping and continuation options is key to predictable sequencing.

Basic Continuation Semantics

A continuation starts after antecedent task reaches a terminal state. Terminal state can be success, fault, or cancellation unless restricted by options.

csharp
1using System;
2using System.Threading.Tasks;
3
4Task first = Task.Run(() => Console.WriteLine("first"));
5Task second = first.ContinueWith(_ => Console.WriteLine("second"));
6
7second.Wait();

If you do not wait or await second, process shutdown may happen before continuation executes.

The Nested Task Wrapper Problem

A common sequencing bug is returning a task from a continuation, producing a nested task wrapper.

csharp
1using System;
2using System.Threading.Tasks;
3
4Task<Task> nested = Task.Run(async () =>
5{
6    await Task.Delay(100);
7}).ContinueWith(_ => Task.Delay(200));
8
9// Wrong: waiting only outer task can finish early
10nested.Wait();

Fix by flattening with Unwrap or by refactoring to await.

csharp
Task flattened = nested.Unwrap();
flattened.Wait();

Now completion waits for inner delayed task too.

Prefer async and await for Readability

Most continuation chains become clearer and safer with await.

csharp
1using System;
2using System.Threading.Tasks;
3
4static async Task RunSequenceAsync()
5{
6    await Task.Delay(100);
7    Console.WriteLine("first done");
8
9    await Task.Delay(100);
10    Console.WriteLine("second done");
11}

This style propagates exceptions naturally and reduces accidental nested task bugs.

Use Continuation Options Explicitly

If continuation should run only on success or only on fault, declare that intent.

csharp
1Task.Run(() => throw new InvalidOperationException("boom"))
2    .ContinueWith(t => Console.WriteLine(t.Exception?.GetBaseException().Message),
3        TaskContinuationOptions.OnlyOnFaulted)
4    .Wait();

Without options, continuation executes on any terminal state, which can hide logic errors.

Synchronization Context Considerations

In UI apps, continuations may resume on thread pool by default, not UI thread. If continuation updates UI state, use TaskScheduler.FromCurrentSynchronizationContext() or prefer await on UI context methods.

Unexpected scheduler behavior can look like ordering issues when in reality UI updates are blocked or cross-thread exceptions are thrown.

Debug Workflow for Sequence Bugs

Use this checklist:

  1. Print type of each task to detect nested tasks.
  2. Confirm which task is awaited.
  3. Add explicit continuation options.
  4. Validate cancellation and fault paths.
  5. Replace chain with await version and compare behavior.

This isolates whether issue is scheduling, unwrapping, or missing await.

Migration Strategy for Legacy Code

When modernizing legacy TPL code, convert one chain at a time:

  • Keep behavior tests around each chain.
  • Replace ContinueWith with async methods incrementally.
  • Remove blocking waits after conversion.

Incremental migration reduces risk compared to full async refactors in one commit.

When ContinueWith Is Still Reasonable

ContinueWith can still be useful in low-level libraries where you need explicit task scheduler control or continuation options that map directly to engine behavior. Even there, keep wrappers small and document why await was not chosen. Most application code remains easier to reason about with async methods.

Common Pitfalls

  • Waiting on an outer nested task while inner work is still running.
  • Forgetting to await the continuation task and exiting early.
  • Mixing blocking Wait calls with async flows, causing deadlocks.
  • Omitting continuation options and running handlers in unintended states.
  • Updating UI from continuations running on thread-pool contexts.

Summary

  • 'ContinueWith respects antecedent completion, but you must observe the correct task.'
  • Flatten nested continuations with Unwrap when tasks return tasks.
  • Prefer async and await for clearer sequencing and error propagation.
  • Use continuation options to define explicit success or fault behavior.
  • Debug ordering issues by inspecting task types, await points, and scheduler context.

Course illustration
Course illustration

All Rights Reserved.