exception handling
Task.ContinueWith
C# programming
asynchronous programming
error handling

Proper way of handling exception in task continuewith

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

Handling exceptions with Task.ContinueWith in .NET can be tricky because faults are captured on the antecedent task and surfaced via AggregateException. Modern code usually prefers async/await with try/catch because it is clearer and safer. If you still use ContinueWith, configure continuation options carefully and always observe faulted tasks.

Core Sections

1) ContinueWith fault handling pattern

csharp
1Task.Run(() => DoWork())
2    .ContinueWith(t =>
3    {
4        // Observe exception
5        Console.WriteLine(t.Exception?.Flatten().InnerException?.Message);
6    }, TaskContinuationOptions.OnlyOnFaulted);

OnlyOnFaulted ensures this continuation runs only for failures.

2) Success/failure split continuations

csharp
1var task = Task.Run(() => Compute());
2
3task.ContinueWith(t => Console.WriteLine(t.Result),
4    TaskContinuationOptions.OnlyOnRanToCompletion);
5
6task.ContinueWith(t => LogError(t.Exception),
7    TaskContinuationOptions.OnlyOnFaulted);

This avoids checking status flags manually inside one continuation.

3) Preferred approach with async/await

csharp
1try
2{
3    var value = await Task.Run(() => Compute());
4    Console.WriteLine(value);
5}
6catch (Exception ex)
7{
8    LogError(ex);
9}

This is generally easier to read and compose.

4) Avoid unobserved task exceptions

Unobserved faults can trigger process-level handlers later. Always await tasks or inspect t.Exception in fault continuations.

csharp
1TaskScheduler.UnobservedTaskException += (s, e) => {
2    LogError(e.Exception);
3    e.SetObserved();
4};

Use this as safety net, not primary error strategy.

Validation and Production Readiness

After implementing any fix or pattern from this topic, validate behavior using a repeatable workflow rather than ad hoc spot checks. The most reliable process has three stages: reproduce baseline behavior, apply one focused change, then verify both expected and adjacent scenarios. This avoids false confidence from a single green run and helps isolate which change actually solved the problem.

A practical command-driven template:

bash
1# 1) capture baseline output/state
2./run_case.sh > before.txt
3
4# 2) apply one focused change from this guide
5# edit code/config and keep the diff minimal
6
7# 3) verify behavior and compare outputs
8./run_case.sh > after.txt
9diff -u before.txt after.txt

If your project includes automated tests, convert the original failure into a regression test immediately. This is the fastest way to prevent the same issue from reappearing during later refactors, dependency upgrades, or environment changes.

bash
1# example quality gate sequence
2./lint.sh
3./test.sh
4./smoke.sh

Also validate edge cases explicitly. Many production defects occur not on the nominal path, but on boundary inputs such as empty collections, null/none values, unusual encodings, or large payloads. Define a compact table of edge scenarios and expected outcomes so reviewers can reproduce your checks quickly.

Before rollout, confirm environment parity. A fix that works in local development can fail in staging or production when runtime versions, OS behavior, file systems, networking, or resource limits differ. Capture version metadata and infrastructure assumptions in your PR or runbook.

bash
1# capture runtime context (example)
2python --version
3node --version
4dotnet --info

Finally, define rollback criteria before deployment. If metrics or logs indicate regressions, teams should know exactly which change to revert and what signals trigger that decision. This operational discipline turns one-off troubleshooting into a maintainable engineering practice and significantly reduces incident recovery time.

Common Pitfalls

  • Using ContinueWith without OnlyOnFaulted and forgetting status checks.
  • Ignoring AggregateException and missing root cause details.
  • Mixing continuation scheduling contexts unintentionally.
  • Writing continuation-heavy code where await would be simpler.
  • Leaving faulted tasks unobserved in fire-and-forget patterns.

Summary

ContinueWith can handle exceptions correctly, but it requires explicit continuation options and exception observation. In most modern .NET code, async/await with try/catch is the clearer default. If continuations are required, split success and fault paths and observe every task failure deterministically.

In production workflows, keep a short checklist of assumptions (runtime version, input shape, and failure-mode expectations) near this logic and verify it during CI. Small compatibility drifts are a common source of regressions even when code compiles successfully. Re-running a focused smoke test after dependency or infrastructure changes is a low-cost way to catch issues before they reach users.


Related reading
Course
Intermediate
27 lessons
14 hours
OOD Fundamentals

Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.