From 36608d5e00956c423391c9f0cdd3d9d05e627d1f Mon Sep 17 00:00:00 2001 From: Bill Wagner Date: Tue, 14 Apr 2026 17:25:16 -0400 Subject: [PATCH 1/5] Task completion and exception handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Create `task-exception-handling.md` — from "Task exception handling in .NET 4.5." Covers `GetAwaiter().GetResult()` vs `.Result` exception propagation, `AggregateException` unwrapping, unobserved task exceptions, `TaskScheduler.UnobservedTaskException`. **Update:** modern .NET default behavior (unobserved exceptions no longer crash the process). 1. Create `complete-your-tasks.md` — from "Don't forget to complete your tasks." Covers: always complete `TaskCompletionSource` on all paths, common bugs (forgetting `SetException` in catch, dropping `TaskCompletionSource` references during reset). 1. Incorporate FAQ content about `Task.Result` vs `GetAwaiter().GetResult()`. 1. Add both to TOC. --- docs/navigate/advanced-programming/toc.yml | 4 + .../complete-your-tasks.md | 63 +++++++ .../csharp/CompleteYourTasks.csproj | 10 ++ .../complete-your-tasks/csharp/Program.cs | 164 ++++++++++++++++++ .../vb/CompleteYourTasks.vbproj | 9 + .../complete-your-tasks/vb/Program.vb | 142 +++++++++++++++ .../task-exception-handling/csharp/Program.cs | 102 +++++++++++ .../csharp/TaskExceptionHandling.csproj | 10 ++ .../task-exception-handling/vb/Program.vb | 85 +++++++++ .../vb/TaskExceptionHandling.vbproj | 9 + .../task-exception-handling.md | 63 +++++++ 11 files changed, 661 insertions(+) create mode 100644 docs/standard/asynchronous-programming-patterns/complete-your-tasks.md create mode 100644 docs/standard/asynchronous-programming-patterns/snippets/complete-your-tasks/csharp/CompleteYourTasks.csproj create mode 100644 docs/standard/asynchronous-programming-patterns/snippets/complete-your-tasks/csharp/Program.cs create mode 100644 docs/standard/asynchronous-programming-patterns/snippets/complete-your-tasks/vb/CompleteYourTasks.vbproj create mode 100644 docs/standard/asynchronous-programming-patterns/snippets/complete-your-tasks/vb/Program.vb create mode 100644 docs/standard/asynchronous-programming-patterns/snippets/task-exception-handling/csharp/Program.cs create mode 100644 docs/standard/asynchronous-programming-patterns/snippets/task-exception-handling/csharp/TaskExceptionHandling.csproj create mode 100644 docs/standard/asynchronous-programming-patterns/snippets/task-exception-handling/vb/Program.vb create mode 100644 docs/standard/asynchronous-programming-patterns/snippets/task-exception-handling/vb/TaskExceptionHandling.vbproj create mode 100644 docs/standard/asynchronous-programming-patterns/task-exception-handling.md diff --git a/docs/navigate/advanced-programming/toc.yml b/docs/navigate/advanced-programming/toc.yml index b69dbd5eaf957..8ff41827d9fb4 100644 --- a/docs/navigate/advanced-programming/toc.yml +++ b/docs/navigate/advanced-programming/toc.yml @@ -30,6 +30,10 @@ items: href: ../../standard/asynchronous-programming-patterns/common-async-bugs.md - name: Async lambda pitfalls href: ../../standard/asynchronous-programming-patterns/async-lambda-pitfalls.md + - name: Task exception handling + href: ../../standard/asynchronous-programming-patterns/task-exception-handling.md + - name: Complete your tasks + href: ../../standard/asynchronous-programming-patterns/complete-your-tasks.md - name: Event-based asynchronous pattern (EAP) items: - name: Documentation overview diff --git a/docs/standard/asynchronous-programming-patterns/complete-your-tasks.md b/docs/standard/asynchronous-programming-patterns/complete-your-tasks.md new file mode 100644 index 0000000000000..a47718f197193 --- /dev/null +++ b/docs/standard/asynchronous-programming-patterns/complete-your-tasks.md @@ -0,0 +1,63 @@ +--- +title: "Complete your tasks" +description: Learn how to complete TaskCompletionSource tasks on every code path, avoid hangs, and handle reset scenarios safely. +ms.date: 04/14/2026 +ai-usage: ai-assisted +dev_langs: + - "csharp" + - "vb" +helpviewer_keywords: + - "TaskCompletionSource" + - "SetException" + - "TrySetResult" + - "async hangs" + - "resettable async primitives" +--- + +# Complete your tasks + +When you expose a task from , you own the task's lifetime. Complete that task on every path. If any path skips completion, callers wait forever. + +## Complete every code path + +This bug appears often: code catches an exception, logs it, and forgets to call `SetException` or `TrySetException`. + +:::code language="csharp" source="./snippets/complete-your-tasks/csharp/Program.cs" id="MissingSetExceptionBug"::: +:::code language="vb" source="./snippets/complete-your-tasks/vb/Program.vb" id="MissingSetExceptionBug"::: + +Fix the bug by completing the task in success and failure paths. Use a `finally` block for cleanup logic that must always run. + +:::code language="csharp" source="./snippets/complete-your-tasks/csharp/Program.cs" id="MissingSetExceptionFix"::: +:::code language="vb" source="./snippets/complete-your-tasks/vb/Program.vb" id="MissingSetExceptionFix"::: + +## Prefer `TrySet*` in completion races + +Concurrent paths often race to complete the same `TaskCompletionSource`. `SetResult`, `SetException`, and `SetCanceled` throw if the task already completed. In race-prone code, use `TrySetResult`, `TrySetException`, and `TrySetCanceled`. + +:::code language="csharp" source="./snippets/complete-your-tasks/csharp/Program.cs" id="TrySetRace"::: +:::code language="vb" source="./snippets/complete-your-tasks/vb/Program.vb" id="TrySetRace"::: + +## Don't drop references during reset + +Another common bug appears in resettable async primitives. If you replace a `TaskCompletionSource` instance before completing the previous one, waiters that hold the old task might never complete. + +:::code language="csharp" source="./snippets/complete-your-tasks/csharp/Program.cs" id="ResetBug"::: +:::code language="vb" source="./snippets/complete-your-tasks/vb/Program.vb" id="ResetBug"::: + +Fix the reset path by atomically swapping references and completing the previous task (for example, with cancellation). + +:::code language="csharp" source="./snippets/complete-your-tasks/csharp/Program.cs" id="ResetFix"::: +:::code language="vb" source="./snippets/complete-your-tasks/vb/Program.vb" id="ResetFix"::: + +## Checklist + +- Complete every exposed `TaskCompletionSource` task on success, failure, and cancellation paths. +- Use `TrySet*` APIs in paths that might race. +- During reset, complete or cancel the old task before you drop its reference. +- Add timeout-based tests so hangs fail fast in CI. + +## See also + +- [Task exception handling](task-exception-handling.md) +- [Implement the TAP](implementing-the-task-based-asynchronous-pattern.md) +- [Common async/await bugs](common-async-bugs.md) diff --git a/docs/standard/asynchronous-programming-patterns/snippets/complete-your-tasks/csharp/CompleteYourTasks.csproj b/docs/standard/asynchronous-programming-patterns/snippets/complete-your-tasks/csharp/CompleteYourTasks.csproj new file mode 100644 index 0000000000000..dfb40caafcf9a --- /dev/null +++ b/docs/standard/asynchronous-programming-patterns/snippets/complete-your-tasks/csharp/CompleteYourTasks.csproj @@ -0,0 +1,10 @@ + + + + Exe + net10.0 + enable + enable + + + diff --git a/docs/standard/asynchronous-programming-patterns/snippets/complete-your-tasks/csharp/Program.cs b/docs/standard/asynchronous-programming-patterns/snippets/complete-your-tasks/csharp/Program.cs new file mode 100644 index 0000000000000..749c1d582ae71 --- /dev/null +++ b/docs/standard/asynchronous-programming-patterns/snippets/complete-your-tasks/csharp/Program.cs @@ -0,0 +1,164 @@ +using System.Threading; + +// +public sealed class MissingSetExceptionBug +{ + public Task StartAsync(bool fail) + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + try + { + if (fail) + { + throw new InvalidOperationException("Simulated failure"); + } + + tcs.SetResult("success"); + } + catch (Exception) + { + // BUG: forgot SetException or TrySetException. + } + + return tcs.Task; + } +} +// + +// +public sealed class MissingSetExceptionFix +{ + public Task StartAsync(bool fail) + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + try + { + if (fail) + { + throw new InvalidOperationException("Simulated failure"); + } + + tcs.TrySetResult("success"); + } + catch (Exception ex) + { + tcs.TrySetException(ex); + } + + return tcs.Task; + } +} +// + +// +public static class TrySetRaceExample +{ + public static void ShowRaceSafeCompletion() + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + bool first = tcs.TrySetResult(42); + bool second = tcs.TrySetException(new TimeoutException("Too late")); + + Console.WriteLine($"First completion won: {first}"); + Console.WriteLine($"Second completion accepted: {second}"); + Console.WriteLine($"Result: {tcs.Task.Result}"); + } +} +// + +// +public sealed class ResetBug +{ + private TaskCompletionSource _signal = NewSignal(); + + public Task WaitAsync() => _signal.Task; + + public void Reset() + { + // BUG: waiters on the old task might never complete. + _signal = NewSignal(); + } + + public void Pulse() + { + _signal.TrySetResult(true); + } + + private static TaskCompletionSource NewSignal() => + new(TaskCreationOptions.RunContinuationsAsynchronously); +} +// + +// +public sealed class ResetFix +{ + private TaskCompletionSource _signal = NewSignal(); + + public Task WaitAsync() => _signal.Task; + + public void Reset() + { + TaskCompletionSource previous = Interlocked.Exchange(ref _signal, NewSignal()); + previous.TrySetCanceled(); + } + + public void Pulse() + { + _signal.TrySetResult(true); + } + + private static TaskCompletionSource NewSignal() => + new(TaskCreationOptions.RunContinuationsAsynchronously); +} +// + +public static class Program +{ + public static void Main() + { + Console.WriteLine("--- MissingSetExceptionBug ---"); + var buggy = new MissingSetExceptionBug(); + Task buggyTask = buggy.StartAsync(fail: true); + bool completed = buggyTask.Wait(200); + Console.WriteLine($"Task completed: {completed}"); + + Console.WriteLine("--- MissingSetExceptionFix ---"); + var fixedVersion = new MissingSetExceptionFix(); + Task fixedTask = fixedVersion.StartAsync(fail: true); + try + { + fixedTask.GetAwaiter().GetResult(); + } + catch (Exception ex) + { + Console.WriteLine($"Observed failure: {ex.GetType().Name}"); + } + + Console.WriteLine("--- TrySetRace ---"); + TrySetRaceExample.ShowRaceSafeCompletion(); + + Console.WriteLine("--- ResetBug ---"); + var resetBug = new ResetBug(); + Task oldWaiter = resetBug.WaitAsync(); + resetBug.Reset(); + resetBug.Pulse(); + Console.WriteLine($"Original waiter completed: {oldWaiter.Wait(200)}"); + + Console.WriteLine("--- ResetFix ---"); + var resetFix = new ResetFix(); + Task oldWaiterFixed = resetFix.WaitAsync(); + resetFix.Reset(); + resetFix.Pulse(); + try + { + oldWaiterFixed.GetAwaiter().GetResult(); + } + catch (Exception ex) + { + Console.WriteLine($"Original waiter completed with: {ex.GetType().Name}"); + } + } +} diff --git a/docs/standard/asynchronous-programming-patterns/snippets/complete-your-tasks/vb/CompleteYourTasks.vbproj b/docs/standard/asynchronous-programming-patterns/snippets/complete-your-tasks/vb/CompleteYourTasks.vbproj new file mode 100644 index 0000000000000..c82c7949771dc --- /dev/null +++ b/docs/standard/asynchronous-programming-patterns/snippets/complete-your-tasks/vb/CompleteYourTasks.vbproj @@ -0,0 +1,9 @@ + + + + Exe + CompleteYourTasks + net10.0 + + + diff --git a/docs/standard/asynchronous-programming-patterns/snippets/complete-your-tasks/vb/Program.vb b/docs/standard/asynchronous-programming-patterns/snippets/complete-your-tasks/vb/Program.vb new file mode 100644 index 0000000000000..1a3da9f5fcf06 --- /dev/null +++ b/docs/standard/asynchronous-programming-patterns/snippets/complete-your-tasks/vb/Program.vb @@ -0,0 +1,142 @@ +Imports System.Threading + +' +Public NotInheritable Class MissingSetExceptionBug + Public Function StartAsync(fail As Boolean) As Task(Of String) + Dim tcs = New TaskCompletionSource(Of String)(TaskCreationOptions.RunContinuationsAsynchronously) + + Try + If fail Then + Throw New InvalidOperationException("Simulated failure") + End If + + tcs.SetResult("success") + Catch ex As Exception + ' BUG: forgot SetException or TrySetException. + End Try + + Return tcs.Task + End Function +End Class +' + +' +Public NotInheritable Class MissingSetExceptionFix + Public Function StartAsync(fail As Boolean) As Task(Of String) + Dim tcs = New TaskCompletionSource(Of String)(TaskCreationOptions.RunContinuationsAsynchronously) + + Try + If fail Then + Throw New InvalidOperationException("Simulated failure") + End If + + tcs.TrySetResult("success") + Catch ex As Exception + tcs.TrySetException(ex) + End Try + + Return tcs.Task + End Function +End Class +' + +' +Public Module TrySetRaceExample + Public Sub ShowRaceSafeCompletion() + Dim tcs = New TaskCompletionSource(Of Integer)(TaskCreationOptions.RunContinuationsAsynchronously) + + Dim first As Boolean = tcs.TrySetResult(42) + Dim second As Boolean = tcs.TrySetException(New TimeoutException("Too late")) + + Console.WriteLine($"First completion won: {first}") + Console.WriteLine($"Second completion accepted: {second}") + Console.WriteLine($"Result: {tcs.Task.Result}") + End Sub +End Module +' + +' +Public NotInheritable Class ResetBug + Private _signal As TaskCompletionSource(Of Boolean) = NewSignal() + + Public Function WaitAsync() As Task + Return _signal.Task + End Function + + Public Sub Reset() + ' BUG: waiters on the old task might never complete. + _signal = NewSignal() + End Sub + + Public Sub Pulse() + _signal.TrySetResult(True) + End Sub + + Private Shared Function NewSignal() As TaskCompletionSource(Of Boolean) + Return New TaskCompletionSource(Of Boolean)(TaskCreationOptions.RunContinuationsAsynchronously) + End Function +End Class +' + +' +Public NotInheritable Class ResetFix + Private _signal As TaskCompletionSource(Of Boolean) = NewSignal() + + Public Function WaitAsync() As Task + Return _signal.Task + End Function + + Public Sub Reset() + Dim previous As TaskCompletionSource(Of Boolean) = Interlocked.Exchange(_signal, NewSignal()) + previous.TrySetCanceled() + End Sub + + Public Sub Pulse() + _signal.TrySetResult(True) + End Sub + + Private Shared Function NewSignal() As TaskCompletionSource(Of Boolean) + Return New TaskCompletionSource(Of Boolean)(TaskCreationOptions.RunContinuationsAsynchronously) + End Function +End Class +' + +Module Program + Sub Main() + Console.WriteLine("--- MissingSetExceptionBug ---") + Dim buggy = New MissingSetExceptionBug() + Dim buggyTask As Task(Of String) = buggy.StartAsync(True) + Dim completed As Boolean = buggyTask.Wait(200) + Console.WriteLine($"Task completed: {completed}") + + Console.WriteLine("--- MissingSetExceptionFix ---") + Dim fixedVersion = New MissingSetExceptionFix() + Dim fixedTask As Task(Of String) = fixedVersion.StartAsync(True) + Try + fixedTask.GetAwaiter().GetResult() + Catch ex As Exception + Console.WriteLine($"Observed failure: {ex.GetType().Name}") + End Try + + Console.WriteLine("--- TrySetRace ---") + TrySetRaceExample.ShowRaceSafeCompletion() + + Console.WriteLine("--- ResetBug ---") + Dim resetBug = New ResetBug() + Dim oldWaiter As Task = resetBug.WaitAsync() + resetBug.Reset() + resetBug.Pulse() + Console.WriteLine($"Original waiter completed: {oldWaiter.Wait(200)}") + + Console.WriteLine("--- ResetFix ---") + Dim resetFix = New ResetFix() + Dim oldWaiterFixed As Task = resetFix.WaitAsync() + resetFix.Reset() + resetFix.Pulse() + Try + oldWaiterFixed.GetAwaiter().GetResult() + Catch ex As Exception + Console.WriteLine($"Original waiter completed with: {ex.GetType().Name}") + End Try + End Sub +End Module diff --git a/docs/standard/asynchronous-programming-patterns/snippets/task-exception-handling/csharp/Program.cs b/docs/standard/asynchronous-programming-patterns/snippets/task-exception-handling/csharp/Program.cs new file mode 100644 index 0000000000000..da5c25d63210d --- /dev/null +++ b/docs/standard/asynchronous-programming-patterns/snippets/task-exception-handling/csharp/Program.cs @@ -0,0 +1,102 @@ +// +public static class SingleExceptionExample +{ + public static Task FaultAsync() + { + return Task.FromException(new InvalidOperationException("Single failure")); + } + + public static void ShowBlockingDifferences() + { + try + { + _ = FaultAsync().Result; + } + catch (AggregateException ex) + { + Console.WriteLine($".Result threw {ex.GetType().Name} with inner {ex.InnerException?.GetType().Name}"); + } + + try + { + _ = FaultAsync().GetAwaiter().GetResult(); + } + catch (Exception ex) + { + Console.WriteLine($"GetAwaiter().GetResult() threw {ex.GetType().Name}"); + } + } +} +// + +// +public static class MultiExceptionExample +{ + public static async Task FaultAfterDelayAsync(string name, int milliseconds) + { + await Task.Delay(milliseconds); + throw new InvalidOperationException($"{name} failed"); + } + + public static void ShowMultipleExceptions() + { + Task combined = Task.WhenAll( + FaultAfterDelayAsync("First", 10), + FaultAfterDelayAsync("Second", 20)); + + try + { + combined.GetAwaiter().GetResult(); + } + catch (Exception ex) + { + Console.WriteLine($"GetAwaiter().GetResult() surfaced: {ex.Message}"); + } + + AggregateException allErrors = combined.Exception!.Flatten(); + Console.WriteLine($"Task.Exception contains {allErrors.InnerExceptions.Count} exceptions."); + } +} +// + +// +public static class UnobservedTaskExceptionExample +{ + public static void ShowEventBehavior() + { + bool eventRaised = false; + + TaskScheduler.UnobservedTaskException += (_, args) => + { + eventRaised = true; + Console.WriteLine($"UnobservedTaskException raised with {args.Exception.InnerExceptions.Count} exception(s)."); + args.SetObserved(); + }; + + _ = Task.Run(() => throw new ApplicationException("Background failure")); + + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + + Console.WriteLine(eventRaised + ? "Event was raised. The process continued." + : "Event was not observed in this short run. The process still continued."); + } +} +// + +public static class Program +{ + public static void Main() + { + Console.WriteLine("--- SingleException ---"); + SingleExceptionExample.ShowBlockingDifferences(); + + Console.WriteLine("--- MultiException ---"); + MultiExceptionExample.ShowMultipleExceptions(); + + Console.WriteLine("--- UnobservedTaskException ---"); + UnobservedTaskExceptionExample.ShowEventBehavior(); + } +} diff --git a/docs/standard/asynchronous-programming-patterns/snippets/task-exception-handling/csharp/TaskExceptionHandling.csproj b/docs/standard/asynchronous-programming-patterns/snippets/task-exception-handling/csharp/TaskExceptionHandling.csproj new file mode 100644 index 0000000000000..dfb40caafcf9a --- /dev/null +++ b/docs/standard/asynchronous-programming-patterns/snippets/task-exception-handling/csharp/TaskExceptionHandling.csproj @@ -0,0 +1,10 @@ + + + + Exe + net10.0 + enable + enable + + + diff --git a/docs/standard/asynchronous-programming-patterns/snippets/task-exception-handling/vb/Program.vb b/docs/standard/asynchronous-programming-patterns/snippets/task-exception-handling/vb/Program.vb new file mode 100644 index 0000000000000..d1f2e2338bafa --- /dev/null +++ b/docs/standard/asynchronous-programming-patterns/snippets/task-exception-handling/vb/Program.vb @@ -0,0 +1,85 @@ +' +Public Module SingleExceptionExample + Public Function FaultAsync() As Task(Of Integer) + Return Task.FromException(Of Integer)(New InvalidOperationException("Single failure")) + End Function + + Public Sub ShowBlockingDifferences() + Try + Dim ignored = FaultAsync().Result + Catch ex As AggregateException + Console.WriteLine($".Result threw {ex.GetType().Name} with inner {ex.InnerException?.GetType().Name}") + End Try + + Try + Dim ignored = FaultAsync().GetAwaiter().GetResult() + Catch ex As Exception + Console.WriteLine($"GetAwaiter().GetResult() threw {ex.GetType().Name}") + End Try + End Sub +End Module +' + +' +Public Module MultiExceptionExample + Public Async Function FaultAfterDelayAsync(name As String, milliseconds As Integer) As Task + Await Task.Delay(milliseconds) + Throw New InvalidOperationException($"{name} failed") + End Function + + Public Sub ShowMultipleExceptions() + Dim combined As Task = Task.WhenAll( + FaultAfterDelayAsync("First", 10), + FaultAfterDelayAsync("Second", 20)) + + Try + combined.GetAwaiter().GetResult() + Catch ex As Exception + Console.WriteLine($"GetAwaiter().GetResult() surfaced: {ex.Message}") + End Try + + Dim allErrors As AggregateException = combined.Exception.Flatten() + Console.WriteLine($"Task.Exception contains {allErrors.InnerExceptions.Count} exceptions.") + End Sub +End Module +' + +' +Public Module UnobservedTaskExceptionExample + Public Sub ShowEventBehavior() + Dim eventRaised As Boolean = False + + AddHandler TaskScheduler.UnobservedTaskException, + Sub(sender, args) + eventRaised = True + Console.WriteLine($"UnobservedTaskException raised with {args.Exception.InnerExceptions.Count} exception(s).") + args.SetObserved() + End Sub + + Dim ignored = Task.Run(Sub() Throw New ApplicationException("Background failure")) + + GC.Collect() + GC.WaitForPendingFinalizers() + GC.Collect() + + If eventRaised Then + Console.WriteLine("Event was raised. The process continued.") + Else + Console.WriteLine("Event was not observed in this short run. The process still continued.") + End If + End Sub +End Module +' + +Module Program + Sub Main() + Console.WriteLine("--- SingleException ---") + SingleExceptionExample.ShowBlockingDifferences() + + Console.WriteLine("--- MultiException ---") + MultiExceptionExample.ShowMultipleExceptions() + + Console.WriteLine("--- UnobservedTaskException ---") + UnobservedTaskExceptionExample.ShowEventBehavior() + End Sub +End Module diff --git a/docs/standard/asynchronous-programming-patterns/snippets/task-exception-handling/vb/TaskExceptionHandling.vbproj b/docs/standard/asynchronous-programming-patterns/snippets/task-exception-handling/vb/TaskExceptionHandling.vbproj new file mode 100644 index 0000000000000..8f4c51f97e2cd --- /dev/null +++ b/docs/standard/asynchronous-programming-patterns/snippets/task-exception-handling/vb/TaskExceptionHandling.vbproj @@ -0,0 +1,9 @@ + + + + Exe + TaskExceptionHandling + net10.0 + + + diff --git a/docs/standard/asynchronous-programming-patterns/task-exception-handling.md b/docs/standard/asynchronous-programming-patterns/task-exception-handling.md new file mode 100644 index 0000000000000..47bf528d014e2 --- /dev/null +++ b/docs/standard/asynchronous-programming-patterns/task-exception-handling.md @@ -0,0 +1,63 @@ +--- +title: "Task exception handling" +description: Learn how exceptions propagate through Task APIs, when AggregateException appears, and how unobserved task exceptions behave in modern .NET. +ms.date: 04/14/2026 +ai-usage: ai-assisted +dev_langs: + - "csharp" + - "vb" +helpviewer_keywords: + - "Task.Result" + - "GetAwaiter().GetResult()" + - "AggregateException" + - "TaskScheduler.UnobservedTaskException" + - "Task exception handling" +--- + +# Task exception handling + +Use `await` as your default. `await` gives you natural exception flow, keeps your code readable, and avoids sync-over-async deadlocks. + +Sometimes you still need to block on a `Task`, for example, in legacy synchronous entry points. In those cases, you need to understand how each API surfaces exceptions. + +## Compare exception propagation for blocking APIs + +When a faulted task throws through blocking APIs, the exception type depends on the API: + +- `Task.Result` and `Task.Wait()` throw . +- `task.GetAwaiter().GetResult()` throws the original exception type. + +:::code language="csharp" source="./snippets/task-exception-handling/csharp/Program.cs" id="SingleException"::: +:::code language="vb" source="./snippets/task-exception-handling/vb/Program.vb" id="SingleException"::: + +For tasks that fault with multiple exceptions, `GetAwaiter().GetResult()` still throws one exception, but stores an that contains all inner exceptions. + +:::code language="csharp" source="./snippets/task-exception-handling/csharp/Program.cs" id="MultiException"::: +:::code language="vb" source="./snippets/task-exception-handling/vb/Program.vb" id="MultiException"::: + +## FAQ: `Task.Result` vs `GetAwaiter().GetResult()` + +Use this guidance when you choose between the two APIs: + +- Prefer `await` when you can. It avoids blocking and deadlock risk. +- If you must block and you want original exception types, use `GetAwaiter().GetResult()`. +- If your existing code expects , use `Result` or `Wait()` and inspect `InnerExceptions`. + +These rules affect exception shape only. Both APIs still block the current thread, so both can deadlock on single-threaded environments. + +## Unobserved task exceptions in modern .NET + +The runtime raises when a faulted task gets finalized before code observes its exception. + +In modern .NET, unobserved exceptions no longer crash the process by default. The runtime reports them through the event, and then continues execution. + +:::code language="csharp" source="./snippets/task-exception-handling/csharp/Program.cs" id="UnobservedTaskException"::: +:::code language="vb" source="./snippets/task-exception-handling/vb/Program.vb" id="UnobservedTaskException"::: + +Use the event for diagnostics and telemetry. Don't use the event as a replacement for normal exception handling in async flows. + +## See also + +- [Common async/await bugs](common-async-bugs.md) +- [Consume the TAP](consuming-the-task-based-asynchronous-pattern.md) +- [Exception handling (Task Parallel Library)](../parallel-programming/exception-handling-task-parallel-library.md) From 989e000c6faea6f40f5df60ed29249fc2d4e7400 Mon Sep 17 00:00:00 2001 From: Bill Wagner Date: Wed, 15 Apr 2026 10:48:18 -0400 Subject: [PATCH 2/5] First edit pass Restructure, add links, and add xrefs. --- .../complete-your-tasks.md | 28 ++++++++--------- .../complete-your-tasks/csharp/Program.cs | 2 ++ .../complete-your-tasks/vb/Program.vb | 2 ++ .../task-exception-handling/csharp/Program.cs | 31 ++++++++++++++----- .../task-exception-handling/vb/Program.vb | 26 ++++++++++++---- .../task-exception-handling.md | 18 ++++++----- 6 files changed, 72 insertions(+), 35 deletions(-) diff --git a/docs/standard/asynchronous-programming-patterns/complete-your-tasks.md b/docs/standard/asynchronous-programming-patterns/complete-your-tasks.md index a47718f197193..4f0f9da9e567c 100644 --- a/docs/standard/asynchronous-programming-patterns/complete-your-tasks.md +++ b/docs/standard/asynchronous-programming-patterns/complete-your-tasks.md @@ -16,39 +16,39 @@ helpviewer_keywords: # Complete your tasks -When you expose a task from , you own the task's lifetime. Complete that task on every path. If any path skips completion, callers wait forever. +When you expose a task from , you own the task's lifetime. Complete that task on every path. If any path skips completion, callers wait forever. ## Complete every code path -This bug appears often: code catches an exception, logs it, and forgets to call `SetException` or `TrySetException`. - -:::code language="csharp" source="./snippets/complete-your-tasks/csharp/Program.cs" id="MissingSetExceptionBug"::: -:::code language="vb" source="./snippets/complete-your-tasks/vb/Program.vb" id="MissingSetExceptionBug"::: - -Fix the bug by completing the task in success and failure paths. Use a `finally` block for cleanup logic that must always run. +Always complete the task in success and failure paths. Use a `finally` block for cleanup logic that must always run. Here's the correct approach: :::code language="csharp" source="./snippets/complete-your-tasks/csharp/Program.cs" id="MissingSetExceptionFix"::: :::code language="vb" source="./snippets/complete-your-tasks/vb/Program.vb" id="MissingSetExceptionFix"::: +The following code catches an exception, logs it, and forgets to call or . This bug appears often and causes callers to wait forever. For more details about exception handling with tasks, see [Task exception handling](task-exception-handling.md). + +:::code language="csharp" source="./snippets/complete-your-tasks/csharp/Program.cs" id="MissingSetExceptionBug"::: +:::code language="vb" source="./snippets/complete-your-tasks/vb/Program.vb" id="MissingSetExceptionBug"::: + ## Prefer `TrySet*` in completion races -Concurrent paths often race to complete the same `TaskCompletionSource`. `SetResult`, `SetException`, and `SetCanceled` throw if the task already completed. In race-prone code, use `TrySetResult`, `TrySetException`, and `TrySetCanceled`. +Concurrent paths often race to complete the same `TaskCompletionSource`. , , and throw if the task already completed. In race-prone code, use , , and . For more patterns to avoid in concurrent scenarios, see [Common async/await bugs](common-async-bugs.md). :::code language="csharp" source="./snippets/complete-your-tasks/csharp/Program.cs" id="TrySetRace"::: :::code language="vb" source="./snippets/complete-your-tasks/vb/Program.vb" id="TrySetRace"::: ## Don't drop references during reset -Another common bug appears in resettable async primitives. If you replace a `TaskCompletionSource` instance before completing the previous one, waiters that hold the old task might never complete. - -:::code language="csharp" source="./snippets/complete-your-tasks/csharp/Program.cs" id="ResetBug"::: -:::code language="vb" source="./snippets/complete-your-tasks/vb/Program.vb" id="ResetBug"::: - -Fix the reset path by atomically swapping references and completing the previous task (for example, with cancellation). +A common bug appears in resettable async primitives. Fix the reset path by atomically swapping references and completing the previous task (for example, with cancellation): :::code language="csharp" source="./snippets/complete-your-tasks/csharp/Program.cs" id="ResetFix"::: :::code language="vb" source="./snippets/complete-your-tasks/vb/Program.vb" id="ResetFix"::: +**Don't do this:** If you replace a `TaskCompletionSource` instance before completing the previous one, waiters that hold the old task might never complete. + +:::code language="csharp" source="./snippets/complete-your-tasks/csharp/Program.cs" id="ResetBug"::: +:::code language="vb" source="./snippets/complete-your-tasks/vb/Program.vb" id="ResetBug"::: + ## Checklist - Complete every exposed `TaskCompletionSource` task on success, failure, and cancellation paths. diff --git a/docs/standard/asynchronous-programming-patterns/snippets/complete-your-tasks/csharp/Program.cs b/docs/standard/asynchronous-programming-patterns/snippets/complete-your-tasks/csharp/Program.cs index 749c1d582ae71..9a6542ecacd00 100644 --- a/docs/standard/asynchronous-programming-patterns/snippets/complete-your-tasks/csharp/Program.cs +++ b/docs/standard/asynchronous-programming-patterns/snippets/complete-your-tasks/csharp/Program.cs @@ -1,6 +1,7 @@ using System.Threading; // +// ⚠️ DON'T copy this snippet. It demonstrates a problem that causes hangs. public sealed class MissingSetExceptionBug { public Task StartAsync(bool fail) @@ -70,6 +71,7 @@ public static void ShowRaceSafeCompletion() // // +// ⚠️ DON'T copy this snippet. It demonstrates a problem where old waiters never complete. public sealed class ResetBug { private TaskCompletionSource _signal = NewSignal(); diff --git a/docs/standard/asynchronous-programming-patterns/snippets/complete-your-tasks/vb/Program.vb b/docs/standard/asynchronous-programming-patterns/snippets/complete-your-tasks/vb/Program.vb index 1a3da9f5fcf06..87cc3505aef33 100644 --- a/docs/standard/asynchronous-programming-patterns/snippets/complete-your-tasks/vb/Program.vb +++ b/docs/standard/asynchronous-programming-patterns/snippets/complete-your-tasks/vb/Program.vb @@ -1,6 +1,7 @@ Imports System.Threading ' +' ⚠️ DON'T copy this snippet. It demonstrates a problem that causes hangs. Public NotInheritable Class MissingSetExceptionBug Public Function StartAsync(fail As Boolean) As Task(Of String) Dim tcs = New TaskCompletionSource(Of String)(TaskCreationOptions.RunContinuationsAsynchronously) @@ -56,6 +57,7 @@ End Module ' ' +' ⚠️ DON'T copy this snippet. It demonstrates a problem where old waiters never complete. Public NotInheritable Class ResetBug Private _signal As TaskCompletionSource(Of Boolean) = NewSignal() diff --git a/docs/standard/asynchronous-programming-patterns/snippets/task-exception-handling/csharp/Program.cs b/docs/standard/asynchronous-programming-patterns/snippets/task-exception-handling/csharp/Program.cs index da5c25d63210d..85a37504282d7 100644 --- a/docs/standard/asynchronous-programming-patterns/snippets/task-exception-handling/csharp/Program.cs +++ b/docs/standard/asynchronous-programming-patterns/snippets/task-exception-handling/csharp/Program.cs @@ -10,24 +10,38 @@ public static void ShowBlockingDifferences() { try { - _ = FaultAsync().Result; + _ = FaultAsync().GetAwaiter().GetResult(); } - catch (AggregateException ex) + catch (Exception ex) { - Console.WriteLine($".Result threw {ex.GetType().Name} with inner {ex.InnerException?.GetType().Name}"); + Console.WriteLine($"GetAwaiter().GetResult() threw {ex.GetType().Name}"); } + } +} +// +// +// ⚠️ DON'T copy this snippet. It demonstrates a problem where exceptions get wrapped unnecessarily. +public static class SingleExceptionBadExample +{ + public static Task FaultAsync() + { + return Task.FromException(new InvalidOperationException("Single failure")); + } + + public static void ShowBlockingDifferences() + { try { - _ = FaultAsync().GetAwaiter().GetResult(); + _ = FaultAsync().Result; } - catch (Exception ex) + catch (AggregateException ex) { - Console.WriteLine($"GetAwaiter().GetResult() threw {ex.GetType().Name}"); + Console.WriteLine($".Result threw {ex.GetType().Name} with inner {ex.InnerException?.GetType().Name}"); } } } -// +// // public static class MultiExceptionExample @@ -93,6 +107,9 @@ public static void Main() Console.WriteLine("--- SingleException ---"); SingleExceptionExample.ShowBlockingDifferences(); + Console.WriteLine("--- SingleExceptionBad ---"); + SingleExceptionBadExample.ShowBlockingDifferences(); + Console.WriteLine("--- MultiException ---"); MultiExceptionExample.ShowMultipleExceptions(); diff --git a/docs/standard/asynchronous-programming-patterns/snippets/task-exception-handling/vb/Program.vb b/docs/standard/asynchronous-programming-patterns/snippets/task-exception-handling/vb/Program.vb index d1f2e2338bafa..96cd44cc71cdc 100644 --- a/docs/standard/asynchronous-programming-patterns/snippets/task-exception-handling/vb/Program.vb +++ b/docs/standard/asynchronous-programming-patterns/snippets/task-exception-handling/vb/Program.vb @@ -5,12 +5,6 @@ Public Module SingleExceptionExample End Function Public Sub ShowBlockingDifferences() - Try - Dim ignored = FaultAsync().Result - Catch ex As AggregateException - Console.WriteLine($".Result threw {ex.GetType().Name} with inner {ex.InnerException?.GetType().Name}") - End Try - Try Dim ignored = FaultAsync().GetAwaiter().GetResult() Catch ex As Exception @@ -20,6 +14,23 @@ Public Module SingleExceptionExample End Module ' +' +' ⚠️ DON'T copy this snippet. It demonstrates a problem where exceptions get wrapped unnecessarily. +Public Module SingleExceptionBadExample + Public Function FaultAsync() As Task(Of Integer) + Return Task.FromException(Of Integer)(New InvalidOperationException("Single failure")) + End Function + + Public Sub ShowBlockingDifferences() + Try + Dim ignored = FaultAsync().Result + Catch ex As AggregateException + Console.WriteLine($".Result threw {ex.GetType().Name} with inner {ex.InnerException?.GetType().Name}") + End Try + End Sub +End Module +' + ' Public Module MultiExceptionExample Public Async Function FaultAfterDelayAsync(name As String, milliseconds As Integer) As Task @@ -76,6 +87,9 @@ Module Program Console.WriteLine("--- SingleException ---") SingleExceptionExample.ShowBlockingDifferences() + Console.WriteLine("--- SingleExceptionBad ---") + SingleExceptionBadExample.ShowBlockingDifferences() + Console.WriteLine("--- MultiException ---") MultiExceptionExample.ShowMultipleExceptions() diff --git a/docs/standard/asynchronous-programming-patterns/task-exception-handling.md b/docs/standard/asynchronous-programming-patterns/task-exception-handling.md index 47bf528d014e2..0bc27eaebba04 100644 --- a/docs/standard/asynchronous-programming-patterns/task-exception-handling.md +++ b/docs/standard/asynchronous-programming-patterns/task-exception-handling.md @@ -18,19 +18,21 @@ helpviewer_keywords: Use `await` as your default. `await` gives you natural exception flow, keeps your code readable, and avoids sync-over-async deadlocks. -Sometimes you still need to block on a `Task`, for example, in legacy synchronous entry points. In those cases, you need to understand how each API surfaces exceptions. +Sometimes you still need to block on a , for example, in legacy synchronous entry points. In those cases, you need to understand how each API surfaces exceptions. ## Compare exception propagation for blocking APIs -When a faulted task throws through blocking APIs, the exception type depends on the API: - -- `Task.Result` and `Task.Wait()` throw . -- `task.GetAwaiter().GetResult()` throws the original exception type. +When you must block on a task, use ().GetResult() to preserve the original exception type: :::code language="csharp" source="./snippets/task-exception-handling/csharp/Program.cs" id="SingleException"::: :::code language="vb" source="./snippets/task-exception-handling/vb/Program.vb" id="SingleException"::: -For tasks that fault with multiple exceptions, `GetAwaiter().GetResult()` still throws one exception, but stores an that contains all inner exceptions. +**Don't do this:** and wrap exceptions in , which complicates exception handling. The following code uses these APIs and receives the wrong exception type: + +:::code language="csharp" source="./snippets/task-exception-handling/csharp/Program.cs" id="SingleExceptionBad"::: +:::code language="vb" source="./snippets/task-exception-handling/vb/Program.vb" id="SingleExceptionBad"::: + +For tasks that fault with multiple exceptions, `GetAwaiter().GetResult()` still throws one exception, but stores an that contains all inner exceptions: :::code language="csharp" source="./snippets/task-exception-handling/csharp/Program.cs" id="MultiException"::: :::code language="vb" source="./snippets/task-exception-handling/vb/Program.vb" id="MultiException"::: @@ -43,11 +45,11 @@ Use this guidance when you choose between the two APIs: - If you must block and you want original exception types, use `GetAwaiter().GetResult()`. - If your existing code expects , use `Result` or `Wait()` and inspect `InnerExceptions`. -These rules affect exception shape only. Both APIs still block the current thread, so both can deadlock on single-threaded environments. +These rules affect exception shape only. Both APIs still block the current thread, so both can deadlock on single-threaded environments. To understand how to properly complete tasks on all code paths, see [Complete your tasks](complete-your-tasks.md). ## Unobserved task exceptions in modern .NET -The runtime raises when a faulted task gets finalized before code observes its exception. +The runtime raises when a faulted `Task` gets finalized before code observes its exception. In modern .NET, unobserved exceptions no longer crash the process by default. The runtime reports them through the event, and then continues execution. From 54ad1aa911b671bbcc405eaa6257d107fb464835 Mon Sep 17 00:00:00 2001 From: Bill Wagner Date: Wed, 15 Apr 2026 13:08:08 -0400 Subject: [PATCH 3/5] fix link --- .../task-exception-handling.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/standard/asynchronous-programming-patterns/task-exception-handling.md b/docs/standard/asynchronous-programming-patterns/task-exception-handling.md index 0bc27eaebba04..fa49e498d4956 100644 --- a/docs/standard/asynchronous-programming-patterns/task-exception-handling.md +++ b/docs/standard/asynchronous-programming-patterns/task-exception-handling.md @@ -27,7 +27,7 @@ When you must block on a task, use and wrap exceptions in , which complicates exception handling. The following code uses these APIs and receives the wrong exception type: + and wrap exceptions in , which complicates exception handling. The following code uses these APIs and receives the wrong exception type: :::code language="csharp" source="./snippets/task-exception-handling/csharp/Program.cs" id="SingleExceptionBad"::: :::code language="vb" source="./snippets/task-exception-handling/vb/Program.vb" id="SingleExceptionBad"::: From 8d279f3b19be67d3e12fac35e1be40b6847a155b Mon Sep 17 00:00:00 2001 From: Bill Wagner Date: Wed, 15 Apr 2026 13:10:07 -0400 Subject: [PATCH 4/5] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../task-exception-handling/csharp/Program.cs | 11 +++++++++-- .../snippets/task-exception-handling/vb/Program.vb | 8 ++++++-- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/docs/standard/asynchronous-programming-patterns/snippets/task-exception-handling/csharp/Program.cs b/docs/standard/asynchronous-programming-patterns/snippets/task-exception-handling/csharp/Program.cs index 85a37504282d7..7b249d3cd89fd 100644 --- a/docs/standard/asynchronous-programming-patterns/snippets/task-exception-handling/csharp/Program.cs +++ b/docs/standard/asynchronous-programming-patterns/snippets/task-exception-handling/csharp/Program.cs @@ -67,8 +67,15 @@ public static void ShowMultipleExceptions() Console.WriteLine($"GetAwaiter().GetResult() surfaced: {ex.Message}"); } - AggregateException allErrors = combined.Exception!.Flatten(); - Console.WriteLine($"Task.Exception contains {allErrors.InnerExceptions.Count} exceptions."); + if (combined.IsFaulted && combined.Exception is not null) + { + AggregateException allErrors = combined.Exception.Flatten(); + Console.WriteLine($"Task.Exception contains {allErrors.InnerExceptions.Count} exceptions."); + } + else + { + Console.WriteLine("Task.Exception is null because the task didn't fault."); + } } } // diff --git a/docs/standard/asynchronous-programming-patterns/snippets/task-exception-handling/vb/Program.vb b/docs/standard/asynchronous-programming-patterns/snippets/task-exception-handling/vb/Program.vb index 96cd44cc71cdc..6be6b17f81a57 100644 --- a/docs/standard/asynchronous-programming-patterns/snippets/task-exception-handling/vb/Program.vb +++ b/docs/standard/asynchronous-programming-patterns/snippets/task-exception-handling/vb/Program.vb @@ -49,8 +49,12 @@ Public Module MultiExceptionExample Console.WriteLine($"GetAwaiter().GetResult() surfaced: {ex.Message}") End Try - Dim allErrors As AggregateException = combined.Exception.Flatten() - Console.WriteLine($"Task.Exception contains {allErrors.InnerExceptions.Count} exceptions.") + If combined.IsFaulted AndAlso combined.Exception IsNot Nothing Then + Dim allErrors As AggregateException = combined.Exception.Flatten() + Console.WriteLine($"Task.Exception contains {allErrors.InnerExceptions.Count} exceptions.") + Else + Console.WriteLine("Task.Exception was not available because the task did not fault.") + End If End Sub End Module ' From 35765bd979b2bb37c39419ea4ac9f9cea4db3c49 Mon Sep 17 00:00:00 2001 From: Bill Wagner Date: Wed, 15 Apr 2026 13:17:30 -0400 Subject: [PATCH 5/5] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../asynchronous-programming-patterns/complete-your-tasks.md | 2 +- .../snippets/task-exception-handling/vb/Program.vb | 2 +- .../task-exception-handling.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/standard/asynchronous-programming-patterns/complete-your-tasks.md b/docs/standard/asynchronous-programming-patterns/complete-your-tasks.md index 4f0f9da9e567c..6314c8207dfd2 100644 --- a/docs/standard/asynchronous-programming-patterns/complete-your-tasks.md +++ b/docs/standard/asynchronous-programming-patterns/complete-your-tasks.md @@ -20,7 +20,7 @@ When you expose a task from stores an that contains all inner exceptions: +For tasks that fault with multiple exceptions, `GetAwaiter().GetResult()` still throws one exception, but stores an that contains all inner exceptions: :::code language="csharp" source="./snippets/task-exception-handling/csharp/Program.cs" id="MultiException"::: :::code language="vb" source="./snippets/task-exception-handling/vb/Program.vb" id="MultiException":::