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
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
| Option | Execution Logic |
| OnlyOnRanToCompletion | Runs only if the antecedent succeeded. |
| NotOnFaulted | Runs only if the antecedent did NOT throw an error. |
| OnlyOnFaulted | Runs only if the antecedent threw an error (Error Handler). |
| OnlyOnCanceled | Runs only if the antecedent was canceled (Cleanup). |
OnlyOnRanToCompletion vs. NotOnFaulted
The fundamental difference lies in how they treat a Canceled task status:
- 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.
- 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 Status | OnlyOnRanToCompletion | NotOnFaulted |
| 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.
Which one should you use?
- Use
OnlyOnRanToCompletionwhen 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. - Use
NotOnFaultedwhen 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.