Skip to content

C# TaskCompletionSource — Complete Guide

DodaTech Updated 2026-06-24 2 min read

In this tutorial, you'll learn about C# TaskCompletionSource. We cover key concepts, practical examples, and best practices.

You have an event-based API (like a socket or a legacy library) that raises an event when done. You want to await its completion instead of using callbacks. TaskCompletionSource<T> creates a Task<T> that you manually control — completing or faulting it as needed.

Wrong

var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
socket.BeginConnect(host, port, ar =>
{
    socket.EndConnect(ar);
    // Now we're connected, but we can't await this
    OnConnected(); // Callback-based
}, null);

Output: Works, but the caller must use callbacks or events, leading to callback nesting.

public Task ConnectAsync(string host, int port)
{
    var tcs = new TaskCompletionSource();
    var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    socket.BeginConnect(host, port, ar =>
    {
        try
        {
            socket.EndConnect(ar);
            tcs.SetResult(); // Signal completion
        }
        catch (Exception ex)
        {
            tcs.SetException(ex); // Signal error
        }
    }, null);
    return tcs.Task;
}

// Usage
await ConnectAsync("example.com", 80);

Output: The event-based APM pattern is wrapped in a clean Task-based API.

TaskCompletionSource supports cancellation, multiple continuations, and running continuations synchronously.

var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);

Prevention

  • Use TaskCompletionSource<T> to bridge old asynchronous patterns (APM, EAP) to Task-based.
  • Use TaskCompletionSource for manual task lifecycle control (e.g., in tests).
  • Use TaskCompletionSource.RunContinuationsAsynchronously to avoid reentrancy issues.
  • Set result on a captured SynchronizationContext if the consumer needs UI thread access.
  • Always use try-catch in the callback to propagate exceptions via SetException.
  • Use TaskCompletionSource.SetCanceled() in cancellation scenarios.

Common Mistakes with task completed source

  1. Misunderstanding that String is [Char] with poor performance for large text operations
  2. Using foldl instead of foldl' causing stack overflow on large lists
  3. Forgetting deriving (Show, Eq) on custom data types needed for debugging

These mistakes appear frequently in real-world CSHARP code. DodaTech's contributors have identified these patterns through analysis of open-source projects and production systems.

Practice Exercise

Write a pure function that safely divides two integers using Maybe, then test it with edge cases like division by zero and negative numbers.

This exercise reinforces the concepts covered in this guide. Try implementing it before checking online solutions.

FAQ

Should I use TaskCompletionSource or Task.Run?

TaskCompletionSource is for wrapping non-Task-based async patterns (events, APM, EAP). Task.Run is for offloading CPU-bound work to the thread pool. They serve different purposes. Do not use TaskCompletionSource to wrap sync code.

What happens if I call SetResult multiple times?

An InvalidOperationException is thrown. A TaskCompletionSource can only transition to a final state once. Check tcs.TrySetResult() (returns bool) if you are not sure whether it already completed.

Can I create a Task that never completes?

new TaskCompletionSource().Task returns a task that never completes unless you call SetResult, SetException, or SetCanceled. This is useful in tests for timeout verification.

TaskCompletionSource is used in Doda Browser to bridge native messaging callbacks to async await. For more C# async, visit DodaTech.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro