Skip to content

Commit 36608d5

Browse files
committed
Task completion and exception handling
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.
1 parent dd25e81 commit 36608d5

11 files changed

Lines changed: 661 additions & 0 deletions

File tree

docs/navigate/advanced-programming/toc.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ items:
3030
href: ../../standard/asynchronous-programming-patterns/common-async-bugs.md
3131
- name: Async lambda pitfalls
3232
href: ../../standard/asynchronous-programming-patterns/async-lambda-pitfalls.md
33+
- name: Task exception handling
34+
href: ../../standard/asynchronous-programming-patterns/task-exception-handling.md
35+
- name: Complete your tasks
36+
href: ../../standard/asynchronous-programming-patterns/complete-your-tasks.md
3337
- name: Event-based asynchronous pattern (EAP)
3438
items:
3539
- name: Documentation overview
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
---
2+
title: "Complete your tasks"
3+
description: Learn how to complete TaskCompletionSource tasks on every code path, avoid hangs, and handle reset scenarios safely.
4+
ms.date: 04/14/2026
5+
ai-usage: ai-assisted
6+
dev_langs:
7+
- "csharp"
8+
- "vb"
9+
helpviewer_keywords:
10+
- "TaskCompletionSource"
11+
- "SetException"
12+
- "TrySetResult"
13+
- "async hangs"
14+
- "resettable async primitives"
15+
---
16+
17+
# Complete your tasks
18+
19+
When you expose a task from <xref:System.Threading.Tasks.TaskCompletionSource`1>, you own the task's lifetime. Complete that task on every path. If any path skips completion, callers wait forever.
20+
21+
## Complete every code path
22+
23+
This bug appears often: code catches an exception, logs it, and forgets to call `SetException` or `TrySetException`.
24+
25+
:::code language="csharp" source="./snippets/complete-your-tasks/csharp/Program.cs" id="MissingSetExceptionBug":::
26+
:::code language="vb" source="./snippets/complete-your-tasks/vb/Program.vb" id="MissingSetExceptionBug":::
27+
28+
Fix the bug by completing the task in success and failure paths. Use a `finally` block for cleanup logic that must always run.
29+
30+
:::code language="csharp" source="./snippets/complete-your-tasks/csharp/Program.cs" id="MissingSetExceptionFix":::
31+
:::code language="vb" source="./snippets/complete-your-tasks/vb/Program.vb" id="MissingSetExceptionFix":::
32+
33+
## Prefer `TrySet*` in completion races
34+
35+
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`.
36+
37+
:::code language="csharp" source="./snippets/complete-your-tasks/csharp/Program.cs" id="TrySetRace":::
38+
:::code language="vb" source="./snippets/complete-your-tasks/vb/Program.vb" id="TrySetRace":::
39+
40+
## Don't drop references during reset
41+
42+
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.
43+
44+
:::code language="csharp" source="./snippets/complete-your-tasks/csharp/Program.cs" id="ResetBug":::
45+
:::code language="vb" source="./snippets/complete-your-tasks/vb/Program.vb" id="ResetBug":::
46+
47+
Fix the reset path by atomically swapping references and completing the previous task (for example, with cancellation).
48+
49+
:::code language="csharp" source="./snippets/complete-your-tasks/csharp/Program.cs" id="ResetFix":::
50+
:::code language="vb" source="./snippets/complete-your-tasks/vb/Program.vb" id="ResetFix":::
51+
52+
## Checklist
53+
54+
- Complete every exposed `TaskCompletionSource` task on success, failure, and cancellation paths.
55+
- Use `TrySet*` APIs in paths that might race.
56+
- During reset, complete or cancel the old task before you drop its reference.
57+
- Add timeout-based tests so hangs fail fast in CI.
58+
59+
## See also
60+
61+
- [Task exception handling](task-exception-handling.md)
62+
- [Implement the TAP](implementing-the-task-based-asynchronous-pattern.md)
63+
- [Common async/await bugs](common-async-bugs.md)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net10.0</TargetFramework>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
</PropertyGroup>
9+
10+
</Project>
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
using System.Threading;
2+
3+
// <MissingSetExceptionBug>
4+
public sealed class MissingSetExceptionBug
5+
{
6+
public Task<string> StartAsync(bool fail)
7+
{
8+
var tcs = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
9+
10+
try
11+
{
12+
if (fail)
13+
{
14+
throw new InvalidOperationException("Simulated failure");
15+
}
16+
17+
tcs.SetResult("success");
18+
}
19+
catch (Exception)
20+
{
21+
// BUG: forgot SetException or TrySetException.
22+
}
23+
24+
return tcs.Task;
25+
}
26+
}
27+
// </MissingSetExceptionBug>
28+
29+
// <MissingSetExceptionFix>
30+
public sealed class MissingSetExceptionFix
31+
{
32+
public Task<string> StartAsync(bool fail)
33+
{
34+
var tcs = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
35+
36+
try
37+
{
38+
if (fail)
39+
{
40+
throw new InvalidOperationException("Simulated failure");
41+
}
42+
43+
tcs.TrySetResult("success");
44+
}
45+
catch (Exception ex)
46+
{
47+
tcs.TrySetException(ex);
48+
}
49+
50+
return tcs.Task;
51+
}
52+
}
53+
// </MissingSetExceptionFix>
54+
55+
// <TrySetRace>
56+
public static class TrySetRaceExample
57+
{
58+
public static void ShowRaceSafeCompletion()
59+
{
60+
var tcs = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously);
61+
62+
bool first = tcs.TrySetResult(42);
63+
bool second = tcs.TrySetException(new TimeoutException("Too late"));
64+
65+
Console.WriteLine($"First completion won: {first}");
66+
Console.WriteLine($"Second completion accepted: {second}");
67+
Console.WriteLine($"Result: {tcs.Task.Result}");
68+
}
69+
}
70+
// </TrySetRace>
71+
72+
// <ResetBug>
73+
public sealed class ResetBug
74+
{
75+
private TaskCompletionSource<bool> _signal = NewSignal();
76+
77+
public Task WaitAsync() => _signal.Task;
78+
79+
public void Reset()
80+
{
81+
// BUG: waiters on the old task might never complete.
82+
_signal = NewSignal();
83+
}
84+
85+
public void Pulse()
86+
{
87+
_signal.TrySetResult(true);
88+
}
89+
90+
private static TaskCompletionSource<bool> NewSignal() =>
91+
new(TaskCreationOptions.RunContinuationsAsynchronously);
92+
}
93+
// </ResetBug>
94+
95+
// <ResetFix>
96+
public sealed class ResetFix
97+
{
98+
private TaskCompletionSource<bool> _signal = NewSignal();
99+
100+
public Task WaitAsync() => _signal.Task;
101+
102+
public void Reset()
103+
{
104+
TaskCompletionSource<bool> previous = Interlocked.Exchange(ref _signal, NewSignal());
105+
previous.TrySetCanceled();
106+
}
107+
108+
public void Pulse()
109+
{
110+
_signal.TrySetResult(true);
111+
}
112+
113+
private static TaskCompletionSource<bool> NewSignal() =>
114+
new(TaskCreationOptions.RunContinuationsAsynchronously);
115+
}
116+
// </ResetFix>
117+
118+
public static class Program
119+
{
120+
public static void Main()
121+
{
122+
Console.WriteLine("--- MissingSetExceptionBug ---");
123+
var buggy = new MissingSetExceptionBug();
124+
Task<string> buggyTask = buggy.StartAsync(fail: true);
125+
bool completed = buggyTask.Wait(200);
126+
Console.WriteLine($"Task completed: {completed}");
127+
128+
Console.WriteLine("--- MissingSetExceptionFix ---");
129+
var fixedVersion = new MissingSetExceptionFix();
130+
Task<string> fixedTask = fixedVersion.StartAsync(fail: true);
131+
try
132+
{
133+
fixedTask.GetAwaiter().GetResult();
134+
}
135+
catch (Exception ex)
136+
{
137+
Console.WriteLine($"Observed failure: {ex.GetType().Name}");
138+
}
139+
140+
Console.WriteLine("--- TrySetRace ---");
141+
TrySetRaceExample.ShowRaceSafeCompletion();
142+
143+
Console.WriteLine("--- ResetBug ---");
144+
var resetBug = new ResetBug();
145+
Task oldWaiter = resetBug.WaitAsync();
146+
resetBug.Reset();
147+
resetBug.Pulse();
148+
Console.WriteLine($"Original waiter completed: {oldWaiter.Wait(200)}");
149+
150+
Console.WriteLine("--- ResetFix ---");
151+
var resetFix = new ResetFix();
152+
Task oldWaiterFixed = resetFix.WaitAsync();
153+
resetFix.Reset();
154+
resetFix.Pulse();
155+
try
156+
{
157+
oldWaiterFixed.GetAwaiter().GetResult();
158+
}
159+
catch (Exception ex)
160+
{
161+
Console.WriteLine($"Original waiter completed with: {ex.GetType().Name}");
162+
}
163+
}
164+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<RootNamespace>CompleteYourTasks</RootNamespace>
6+
<TargetFramework>net10.0</TargetFramework>
7+
</PropertyGroup>
8+
9+
</Project>

0 commit comments

Comments
 (0)