Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/navigate/advanced-programming/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <xref:System.Threading.Tasks.TaskCompletionSource%601>, 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 <xref:System.Threading.Tasks.TaskCompletionSource%601.SetException%2A> or <xref:System.Threading.Tasks.TaskCompletionSource%601.TrySetException%2A>. 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`. <xref:System.Threading.Tasks.TaskCompletionSource%601.SetResult%2A>, <xref:System.Threading.Tasks.TaskCompletionSource%601.SetException%2A>, and <xref:System.Threading.Tasks.TaskCompletionSource%601.SetCanceled%2A> throw if the task already completed. In race-prone code, use <xref:System.Threading.Tasks.TaskCompletionSource%601.TrySetResult%2A>, <xref:System.Threading.Tasks.TaskCompletionSource%601.TrySetException%2A>, and <xref:System.Threading.Tasks.TaskCompletionSource%601.TrySetCanceled%2A>. 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)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
using System.Threading;

// <MissingSetExceptionBug>
// ⚠️ DON'T copy this snippet. It demonstrates a problem that causes hangs.
public sealed class MissingSetExceptionBug
{
public Task<string> StartAsync(bool fail)
{
var tcs = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);

try
{
if (fail)
{
throw new InvalidOperationException("Simulated failure");
}

tcs.SetResult("success");
}
catch (Exception)
{
// BUG: forgot SetException or TrySetException.
}

return tcs.Task;
}
}
// </MissingSetExceptionBug>

// <MissingSetExceptionFix>
public sealed class MissingSetExceptionFix
{
public Task<string> StartAsync(bool fail)
{
var tcs = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);

try
{
if (fail)
{
throw new InvalidOperationException("Simulated failure");
}

tcs.TrySetResult("success");
}
catch (Exception ex)
{
tcs.TrySetException(ex);
}

return tcs.Task;
}
}
// </MissingSetExceptionFix>

// <TrySetRace>
public static class TrySetRaceExample
{
public static void ShowRaceSafeCompletion()
{
var tcs = new TaskCompletionSource<int>(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}");
}
}
// </TrySetRace>

// <ResetBug>
// ⚠️ DON'T copy this snippet. It demonstrates a problem where old waiters never complete.
public sealed class ResetBug
{
private TaskCompletionSource<bool> _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<bool> NewSignal() =>
new(TaskCreationOptions.RunContinuationsAsynchronously);
}
// </ResetBug>

// <ResetFix>
public sealed class ResetFix
{
private TaskCompletionSource<bool> _signal = NewSignal();

public Task WaitAsync() => _signal.Task;

public void Reset()
{
TaskCompletionSource<bool> previous = Interlocked.Exchange(ref _signal, NewSignal());
previous.TrySetCanceled();
}

public void Pulse()
{
_signal.TrySetResult(true);
}

private static TaskCompletionSource<bool> NewSignal() =>
new(TaskCreationOptions.RunContinuationsAsynchronously);
}
// </ResetFix>

public static class Program
{
public static void Main()
{
Console.WriteLine("--- MissingSetExceptionBug ---");
var buggy = new MissingSetExceptionBug();
Task<string> buggyTask = buggy.StartAsync(fail: true);
bool completed = buggyTask.Wait(200);
Console.WriteLine($"Task completed: {completed}");

Console.WriteLine("--- MissingSetExceptionFix ---");
var fixedVersion = new MissingSetExceptionFix();
Task<string> 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}");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<RootNamespace>CompleteYourTasks</RootNamespace>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>

</Project>
Loading
Loading