C#

When should TaskCompletionSourceT be used

19 September 2026 · 8 min read

When should TaskCompletionSourceT be used

Understanding asynchronous programming in .NET can be daunting, especially when dealing with intricate scenarios that require fine-grained control over task execution. One powerful tool for these situations is the TaskCompletionSource<T>. But when should TaskCompletionSource<T> be used? It’s not your everyday async/await scenario. Instead, it provides a bridge between synchronous and asynchronous code, enabling you to create tasks that are completed externally, often in response to events or callbacks. Imagine you’re integrating with an older API that doesn’t support async operations natively or building a custom synchronization mechanism. The TaskCompletionSource<T> offers a robust way to wrap these operations in a task, making them compatible with modern asynchronous workflows. This article will delve into the specifics, exploring practical use cases and providing clear guidance on leveraging this valuable class.

Understanding TaskCompletionSource<T>

The TaskCompletionSource<T> class is a crucial component in the .NET asynchronous programming model. It essentially acts as a promise to provide a value (of type T) at some point in the future. Unlike regular tasks created with Task.Run or async methods, a TaskCompletionSource<T> does not inherently execute any code. Instead, it gives you a Task<T> object that you can return to the caller, and you have complete control over when and how that task is completed, faulted, or canceled. This makes it ideal for scenarios where you need to convert event-based or callback-driven operations into task-based asynchronous operations. The power lies in the decoupling; you’re separating the creation of the task from its actual execution and completion.

Think of it as a delivery service. You order something (the task), but the delivery person (your code) decides when and how the package arrives. The TaskCompletionSource<T> provides the box (the Task<T>) and the means to notify you when it’s ready (SetResult, SetException, SetCanceled). According to Microsoft’s documentation, “TaskCompletionSource is useful for interoperating between asynchronous and synchronous code.” [1] This interoperability is where its true value shines, allowing you to bridge the gap between different programming paradigms.

Common Use Cases for TaskCompletionSource<T>

So, when should TaskCompletionSource<T> be used in practice? Several scenarios benefit greatly from its unique capabilities. One prominent example is wrapping event-based APIs. Many older .NET APIs rely on events to signal the completion of an operation. To integrate these APIs into a modern async/await workflow, you can use TaskCompletionSource<T> to create a task that is completed when the event is raised. This allows you to await the completion of the event as if it were a regular asynchronous operation. Imagine you’re working with the FileSystemWatcher class, which uses events to notify you of file system changes. You can wrap these events in tasks using TaskCompletionSource<T> to create an asynchronous file monitoring system.

Another critical use case involves managing custom asynchronous operations or synchronization primitives. If you need to implement a complex asynchronous operation that doesn’t fit the standard async/await pattern, or if you’re building a custom synchronization primitive like a semaphore or a mutex, TaskCompletionSource<T> provides the necessary control. You can manually control the state of the task, setting its result, exception, or cancellation status based on your custom logic. Furthermore, TaskCompletionSource<T> is invaluable when dealing with long-running operations where you need to report progress updates. By exposing the underlying task, you can allow consumers to track the operation’s progress and potentially cancel it if needed. Consider using it for managing complex workflows or background processes.

  • Wrapping event-based APIs for async/await compatibility.
  • Creating custom asynchronous operations and synchronization primitives.

Practical Examples and Implementation

Let’s illustrate when should TaskCompletionSource<T> be used with a practical example. Consider wrapping a Timer that uses a callback. The Timer itself isn’t asynchronous, but we can make it awaitable using TaskCompletionSource<T>. This is a valuable approach for scheduling tasks that need to run at specific intervals without blocking the main thread. This example demonstrates how to transform a synchronous, callback-based operation into an asynchronous one.

Here’s how you might implement it:

  1. Create a TaskCompletionSource<bool>.
  2. Create a Timer instance that calls a callback method.
  3. In the callback method, set the result of the TaskCompletionSource<bool>.
  4. Return the Task from the TaskCompletionSource<bool>.

This allows you to await the timer’s completion, integrating it seamlessly into your asynchronous code. Another example involves creating a custom retry mechanism. You can use TaskCompletionSource<T> to create a task that retries an operation multiple times until it succeeds, setting the result of the task when the operation finally completes successfully. If the operation fails after all retries, you can set the exception on the TaskCompletionSource<T>.

Handling Exceptions and Cancellation

Properly handling exceptions and cancellation is paramount when should TaskCompletionSource<T> be used, to prevent unhandled exceptions and ensure your application remains stable. When an error occurs within the operation you’re wrapping, you should set the exception on the TaskCompletionSource<T> using the SetException method. This will propagate the exception to the awaiting task, allowing it to be handled appropriately. It’s also crucial to implement cancellation support. You can use a CancellationToken to signal that the operation should be canceled. When cancellation is requested, you should set the TaskCompletionSource<T> to a canceled state using the SetCanceled method.

Here’s a paragraph optimized for featured snippets:

TaskCompletionSource<T> should be used when you need to bridge synchronous and asynchronous code, especially when working with event-based APIs or custom synchronization mechanisms. It allows you to create a Task<T> that you can control externally, setting its result, exception, or cancellation status based on your custom logic. This provides fine-grained control over task execution and enables you to integrate legacy code seamlessly into modern asynchronous workflows. Remember to handle exceptions and cancellation properly to ensure the stability and responsiveness of your application.

  • Always handle exceptions and set the exception on the TaskCompletionSource<T>.
  • Implement cancellation support using a CancellationToken.

Best Practices and Considerations

When should TaskCompletionSource<T> be used, keep best practices in mind to ensure your code is robust and maintainable. Avoid using TaskCompletionSource<T> unnecessarily. If you can achieve the same result using standard async/await patterns, that is generally the preferred approach. TaskCompletionSource<T> adds complexity, so it should be reserved for scenarios where it provides a clear advantage. Always ensure that the TaskCompletionSource<T> is completed exactly once. Calling SetResult, SetException, or SetCanceled multiple times can lead to unexpected behavior and potential race conditions. Use the TrySetResult, TrySetException, and TrySetCanceled methods to ensure that the task is only completed once. According to Stephen Cleary, a leading expert in .NET asynchronous programming, “TaskCompletionSource is a powerful tool, but it should be used with caution.” Learn more about asynchronous programming.

Consider the performance implications. While TaskCompletionSource<T> provides flexibility, it can also introduce overhead compared to simpler asynchronous operations. Profile your code to ensure that the use of TaskCompletionSource<T> is not creating a performance bottleneck. Another key consideration is thread safety. If you’re accessing the TaskCompletionSource<T> from multiple threads, you need to ensure that your code is properly synchronized to prevent race conditions. Use locks or other synchronization primitives to protect the TaskCompletionSource<T> from concurrent access. Finally, thoroughly document your code. When using TaskCompletionSource<T>, it’s essential to clearly explain why it’s being used and how it works. This will help other developers (and yourself in the future) understand the code and maintain it effectively. Properly utilizing the ConfigureAwait(false) method is also crucial to avoid deadlocks and improve responsiveness, especially in UI applications. [2]

Infographic illustrating TaskCompletionSource usage scenarios here
FAQ About TaskCompletionSource<T> ---------------------------------------
What is the main purpose of TaskCompletionSource<T>?
The main purpose is to bridge synchronous and asynchronous code by allowing you to create a Task<T> that you can control externally.
When should I avoid using TaskCompletionSource<T>?
Avoid using it when standard async/await patterns can achieve the same result, as it adds complexity.
How do I handle exceptions with TaskCompletionSource<T>?
Use the SetException method to propagate exceptions to the awaiting task.
How do I implement cancellation with TaskCompletionSource<T>?
Use a CancellationToken and the SetCanceled method to signal and handle cancellation requests.
Is TaskCompletionSource<T> thread-safe?
No, you need to ensure thread safety by using locks or other synchronization primitives if accessing it from multiple threads.
By now, you should have a good understanding of **when should TaskCompletionSource<T> be used**. It's a powerful tool for bridging synchronous and asynchronous worlds, especially when dealing with event-based APIs or custom synchronization logic. However, remember that it comes with added complexity, so use it judiciously and always prioritize clarity and maintainability. Consider exploring other asynchronous patterns like async streams for handling sequences of asynchronous data. [\[3\]](https://www.nuget.org/packages/System.Linq.Async/)

If you find yourself wrestling with integrating older APIs or building intricate asynchronous workflows, TaskCompletionSource<T> could be the key to unlocking a more elegant and efficient solution. Don’t hesitate to experiment and explore its capabilities further. Consider reviewing the official Microsoft documentation and consulting with experienced developers to deepen your understanding. Are you ready to take your asynchronous programming skills to the next level?

Question & Answer :
AFAIK, all it knows is that at some point, its SetResult or SetException method is being called to complete the Task<T> exposed through its Task property.

In other words, it acts as the producer for a Task<TResult> and its completion.

I saw here the example:

If I need a way to execute a Func<T> asynchronously and have a Task<T> to represent that operation.

public static Task<T> RunAsync<T>(Func<T> function) { if (function == null) throw new ArgumentNullException(“function”); var tcs = new TaskCompletionSource<T>(); ThreadPool.QueueUserWorkItem(_ => { try { T result = function(); tcs.SetResult(result); } catch(Exception exc) { tcs.SetException(exc); } }); return tcs.Task; } 

Which could be used if I didn’t have Task.Factory.StartNew - But I do have Task.Factory.StartNew.

Question:

Can someone please explain by example a scenario related directly to TaskCompletionSource and not to a hypothetical situation in which I don’t have Task.Factory.StartNew?

I mostly use it when only an event based API is available (for example Windows Phone 8 sockets):

public Task<Args> SomeApiWrapper() { TaskCompletionSource<Args> tcs = new TaskCompletionSource<Args>(); var obj = new SomeApi(); // will get raised, when the work is done obj.Done += (args) => { // this will notify the caller // of the SomeApiWrapper that // the task just completed tcs.SetResult(args); } // start the work obj.Do(); return tcs.Task; } 

So it’s especially useful when used together with the C#5 async keyword.