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

Handling Errors in C# Task Continuations

In asynchronous programming, a task is rarely an isolated event. We chain them together to create functional pipelines. However, when an antecedent task fails, it doesn't just stop; it passes its state—including its errors—down the chain.

Without proper error handling, a single exception can cause "cascading failures," where subsequent tasks crash while trying to process invalid data. Understanding how to manage these failures is the difference between a brittle application and a resilient one.

The Danger of the Result Property

The most common mistake in task continuations is accessing antecedent.Result without checking if the task actually succeeded. If the antecedent faulted (threw an exception), calling .Result or .Wait() will throw an AggregateException.

If your continuation isn't wrapped in a try-catch block, this second exception will crash the continuation task, potentially losing the original error context.

Strategy 1: Manual Guarding with Task Properties

Before touching the result of a task, you should inspect its state. The Task class provides properties like IsFaulted, IsCanceled, and Status to help you navigate this.

Code Example: Safe Data Access

using System;
using System.Threading.Tasks;

class SafeContinuation
{
static void Main()
{
Task<string> fetchData = Task.Run(() => {
// Simulating a failure during data retrieval
throw new TimeoutException("Database connection timed out.");
return "Sensitive Data";
});

fetchData.ContinueWith(antecedent =>
{
if (antecedent.IsFaulted)
{
// Accessing Exception property is safe here
Console.WriteLine($"Log: Task failed with {antecedent.Exception.InnerException.Message}");
}
else if (antecedent.IsCanceled)
{
Console.WriteLine("Log: Task was canceled by the user.");
}
else
{
// Only access Result if we are certain it succeeded
Console.WriteLine($"Data received: {antecedent.Result}");
}
});

Console.ReadLine();
}
}

Strategy 2: Conditional Continuations

Instead of manually checking statuses inside the delegate, you can use TaskContinuationOptions to tell the TPL only to run the continuation if a specific condition is met.

Filtering by Outcome

OptionExecution Logic
OnlyOnRanToCompletionRuns only if the antecedent succeeded.
NotOnFaultedRuns only if the antecedent did NOT throw an error.
OnlyOnFaultedRuns only if the antecedent threw an error (Error Handler).
OnlyOnCanceledRuns only if the antecedent was canceled (Cleanup).


OnlyOnRanToCompletion vs. NotOnFaulted

The fundamental difference lies in how they treat a Canceled task status:

  1. OnlyOnRanToCompletion: This is the strictest option. The continuation triggers only if the antecedent finishes its work successfully. If the task is either faulted (exception) or canceled, the continuation is ignored.
  2. NotOnFaulted: This is a broader filter. It triggers as long as the antecedent did not throw an unhandled exception. This includes tasks that were successful and tasks that were canceled.

Task Outcome Matrix

Antecedent StatusOnlyOnRanToCompletionNotOnFaulted
RanToCompletion (Success)✅ Runs✅ Runs
Faulted (Exception)❌ Skips❌ Skips
Canceled❌ Skips✅ Runs

Practical Example: The Cancellation Scenario

In the following example, we explicitly cancel a task. You will see that the "NotOnFaulted" continuation still executes, while the "OnlyOnRanToCompletion" one does not.

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

class NuanceExample
{
static void Main()
{
var cts = new CancellationTokenSource();
CancellationToken token = cts.Token;

// Create a task that will be canceled immediately
Task antecedent = Task.Run(() =>
{
Console.WriteLine("Task started...");
token.ThrowIfCancellationRequested();
}, token);

// This will NOT run because the task was canceled (not successful)
antecedent.ContinueWith(t =>
Console.WriteLine("--- OnlyOnRanToCompletion: I will NOT run."),
TaskContinuationOptions.OnlyOnRanToCompletion);

// This WILL run because the task is Canceled, and Canceled is "Not Faulted"
antecedent.ContinueWith(t =>
Console.WriteLine("--- NotOnFaulted: I am running even though it was canceled!"),
TaskContinuationOptions.NotOnFaulted);

// Trigger the cancellation
cts.Cancel();

try { antecedent.Wait(); } catch { }
// Brief pause to allow continuations to finish printing
Thread.Sleep(500);
Console.WriteLine("Main thread finished.");
}
}

Which one should you use?

  1. Use OnlyOnRanToCompletion when the continuation depends on a valid result (e.g., saving data to a database that was calculated in the antecedent). If the task was canceled, you likely don't have a result to save.
  2. Use NotOnFaulted when you want to perform cleanup or trigger a subsequent step that should happen regardless of whether the work was finished or just stopped, as long as no "real" error (exception) occurred.
Share this lesson: