High-Performance .NET: Async, Multithreading, and Parallel Programming Tasks in .Net Created: 22 Jan 2026 Updated: 22 Jan 2026

TaskContinuationOptions

While ContinueWith allows us to chain tasks, it often executes blindly—running regardless of whether the previous task succeeded, failed, or was canceled. To build robust asynchronous pipelines, we need surgical control over when and how these continuations run. This is achieved through the TaskContinuationOptions enum.

TaskContinuationOptions provides a set of flags that tell the Task Scheduler how to handle the continuation task. These options can be broadly categorized into conditional execution, scheduling behavior, and hierarchy management.

1. Conditional Execution Options

These are the most frequently used options. They allow you to create "branching" logic in your task chains, where different tasks run based on the final state of the antecedent.

Example: Handling Success, Failure, and Cancellation

In this example, we use a CancellationToken to simulate various outcomes and show how specific continuations respond.

using System;
using System.Threading;
using System.Threading.Tasks;

class ConditionalExecutionDemo
{
static void Main()
{
var cts = new CancellationTokenSource();
Task<int> antecedent = Task.Run(() =>
{
Console.WriteLine("Antecedent working...");
Thread.Sleep(1000);
// Uncomment one of these to test different flows:
// throw new Exception("Something went wrong!"); // Test Faulted
// cts.Cancel(); return 0; // Test Canceled
return 42; // Test Success
}, cts.Token);

// Run ONLY if successful
antecedent.ContinueWith(t =>
Console.WriteLine($"Success! Result: {t.Result}"),
TaskContinuationOptions.OnlyOnRanToCompletion);

// Run ONLY if an exception occurred
antecedent.ContinueWith(t =>
Console.WriteLine($"Error: {t.Exception.InnerException.Message}"),
TaskContinuationOptions.OnlyOnFaulted);

// Run ONLY if canceled
antecedent.ContinueWith(t =>
Console.WriteLine("Task was canceled."),
TaskContinuationOptions.OnlyOnCanceled);

try { antecedent.Wait(); } catch { }
Thread.Sleep(500); // Give continuations time to print
}
}

2. Scheduling and Performance Options

These options influence the Task Scheduler's behavior regarding thread allocation and timing.

  1. ExecuteSynchronously: Hints that the continuation should run on the same thread that completed the antecedent. This reduces context switching for very short-lived tasks.
  2. LongRunning: Tells the scheduler to potentially allocate a dedicated thread rather than a ThreadPool thread. Use this for tasks that block for long periods (like I/O or infinite loops).
  3. PreferFairness: Requests the scheduler to schedule tasks in the order they were queued, rather than using internal optimizations that might favor efficiency over order.

Example: Synchronous vs. Long Running

Task antecedent = Task.Run(() => Console.WriteLine($"Antecedent thread: {Thread.CurrentThread.ManagedThreadId}"));

// ExecuteSynchronously: Often runs on the same thread as the antecedent
antecedent.ContinueWith(t =>
Console.WriteLine($"Sync Continuation thread: {Thread.CurrentThread.ManagedThreadId}"),
TaskContinuationOptions.ExecuteSynchronously);

// LongRunning: Likely to get a new, dedicated thread
antecedent.ContinueWith(t =>
{
Console.WriteLine($"LongRunning thread: {Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep(5000); // Simulated long-held thread
}, TaskContinuationOptions.LongRunning);

3. Hierarchy and Relationship Options

TPL allows tasks to have parent-child relationships. These options control how continuations interact with that hierarchy.

  1. AttachedToParent: If the antecedent is a child task, this continuation will also be attached to the parent. The parent task will not complete until all attached children and their continuations are done.
  2. DenyChildAttach: Prevents any tasks created within the continuation from being attached as children.

Example: AttachedToParent

var parent = Task.Run(() =>
{
var child = Task.Factory.StartNew(() =>
{
Thread.Sleep(1000);
Console.WriteLine("Child finished.");
}, TaskCreationOptions.AttachedToParent);

child.ContinueWith(t =>
{
Thread.Sleep(1000);
Console.WriteLine("Continuation of child (Attached) finished.");
}, TaskContinuationOptions.AttachedToParent);
});

parent.Wait(); // Parent waits for BOTH the child and its continuation
Console.WriteLine("Parent finished.");

Summary Table: Key Options

OptionCategoryEffect
OnlyOnRanToCompletionConditionalExecutes only if the antecedent finishes successfully.
NotOnFaultedConditionalExecutes only if the antecedent did NOT throw an exception.
ExecuteSynchronouslySchedulingRuns on the thread that completed the antecedent.
LongRunningSchedulingRequests a dedicated thread for a long-lived task.
AttachedToParentHierarchyLinks the continuation's lifetime to the parent task.

A Note on Compatibility

It is important to remember that some options are mutually exclusive. For instance, you cannot combine OnlyOnRanToCompletion with NotOnRanToCompletion. Furthermore, as mentioned in the documentation, conditional options are not valid for multi-task continuations (like ContinueWhenAll) because the scheduler wouldn't know which antecedent task's state to check.

Share this lesson: