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..6314c8207dfd2 --- /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 + +Always complete the task in success and failure paths. Use a `catch` block for cleanup logic when the task fails. Use a `finally` block for cleanup logic that must always run. The following code block shows adding cleanup for a failure path: + +:::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`. , , 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 + +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. +- 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..9a6542ecacd00 --- /dev/null +++ b/docs/standard/asynchronous-programming-patterns/snippets/complete-your-tasks/csharp/Program.cs @@ -0,0 +1,166 @@ +using System.Threading; + +// +// ⚠️ DON'T copy this snippet. It demonstrates a problem that causes hangs. +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}"); + } +} +// + +// +// ⚠️ DON'T copy this snippet. It demonstrates a problem where old waiters never complete. +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..87cc3505aef33 --- /dev/null +++ b/docs/standard/asynchronous-programming-patterns/snippets/complete-your-tasks/vb/Program.vb @@ -0,0 +1,144 @@ +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) + + 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 +' + +' +' ⚠️ 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() + + 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..7b249d3cd89fd --- /dev/null +++ b/docs/standard/asynchronous-programming-patterns/snippets/task-exception-handling/csharp/Program.cs @@ -0,0 +1,126 @@ +// +public static class SingleExceptionExample +{ + public static Task FaultAsync() + { + return Task.FromException(new InvalidOperationException("Single failure")); + } + + public static void ShowBlockingDifferences() + { + try + { + _ = FaultAsync().GetAwaiter().GetResult(); + } + catch (Exception ex) + { + 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().Result; + } + catch (AggregateException ex) + { + Console.WriteLine($".Result threw {ex.GetType().Name} with inner {ex.InnerException?.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}"); + } + + 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."); + } + } +} +// + +// +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("--- SingleExceptionBad ---"); + SingleExceptionBadExample.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..6da9c1c1983f6 --- /dev/null +++ b/docs/standard/asynchronous-programming-patterns/snippets/task-exception-handling/vb/Program.vb @@ -0,0 +1,103 @@ +' +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().GetAwaiter().GetResult() + Catch ex As Exception + Console.WriteLine($"GetAwaiter().GetResult() threw {ex.GetType().Name}") + End Try + End Sub +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 + 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 + + 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 +' + +' +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 + + 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("--- SingleExceptionBad ---") + SingleExceptionBadExample.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..a939c549fa0bf --- /dev/null +++ b/docs/standard/asynchronous-programming-patterns/task-exception-handling.md @@ -0,0 +1,65 @@ +--- +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 , 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 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"::: + + 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"::: + +## 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. 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. + +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)