How to Implement Timeouts for Async Tasks in C

In modern C# development, preventing asynchronous operations from hanging indefinitely is critical for building resilient applications. This article provides a straightforward guide on how to implement timeouts for asynchronous tasks using CancellationTokenSource and Task.Delay. You will learn the recommended approach for operations that natively accept cancellation tokens, as well as an alternative pattern for legacy or non-cancellable operations using Task.WhenAny.

Approach 1: Using CancellationTokenSource with a Time Limit

The cleanest and most idiomatic way to implement a timeout in .NET is to use a CancellationTokenSource configured with a specific duration. This pattern works with any asynchronous method that accepts a CancellationToken.

using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

public class TimeoutExample
{
    private static readonly HttpClient httpClient = new HttpClient();

    public static async Task FetchDataWithTimeoutAsync()
    {
        // Set a timeout of 5 seconds
        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));

        try
        {
            HttpResponseMessage response = await httpClient.GetAsync("https://example.com", cts.Token);
            string content = await response.Content.ReadAsStringAsync(cts.Token);
            Console.WriteLine(content);
        }
        catch (OperationCanceledException) when (cts.IsCancellationRequested)
        {
            Console.WriteLine("The operation timed out.");
        }
    }
}

How It Works

  • CancellationTokenSource(TimeSpan) automatically triggers cancellation after the specified time passes.
  • You pass cts.Token to the asynchronous operation.
  • If the operation exceeds the deadline, the token signals cancellation, causing the method to throw an OperationCanceledException (or TaskCanceledException).
  • The when (cts.IsCancellationRequested) filter ensures that you only treat the error as a timeout if the cancellation was initiated by your source rather than an external token.

Approach 2: Using Task.WhenAny and Task.Delay

When working with third-party libraries or legacy code that does not accept a CancellationToken, you can implement a timeout by racing your task against a Task.Delay.

using System;
using System.Threading;
using System.Threading.Tasks;

public class TaskDelayTimeoutExample
{
    public static async Task<TResult> WithTimeout<TResult>(Task<TResult> task, TimeSpan timeout)
    {
        using var timeoutCts = new CancellationTokenSource();
        Task delayTask = Task.Delay(timeout, timeoutCts.Token);

        Task completedTask = await Task.WhenAny(task, delayTask);

        if (completedTask == task)
        {
            // Cancel the delay task to free up timer resources immediately
            timeoutCts.Cancel();
            return await task; // Propagates the result or original exception
        }

        throw new TimeoutException($"The operation exceeded the timeout of {timeout.TotalSeconds} seconds.");
    }
}

Usage

try
{
    // Simulate a third-party non-cancellable task
    Task<string> slowTask = LegacyLongRunningMethodAsync();

    string result = await TaskDelayTimeoutExample.WithTimeout(slowTask, TimeSpan.FromSeconds(3));
    Console.WriteLine(result);
}
catch (TimeoutException ex)
{
    Console.WriteLine(ex.Message);
}

Key Considerations for Task.WhenAny

  • Resource Management: Always cancel the Task.Delay using its own CancellationTokenSource once the primary task finishes. If omitted, the background timer will run until the full timeout duration elapses, which can degrade performance under heavy load.
  • Unobserved Background Work: If the timeout fires first, the original task will continue executing in the background until completion because it cannot be cancelled. Ensure that background execution will not cause data corruption or resource exhaustion.

Alternative: .NET 6+ Task.WaitAsync

If you are using .NET 6 or later, .NET includes a built-in method called WaitAsync that simplifies the Task.WhenAny pattern for single tasks:

try
{
    Task slowTask = LegacyLongRunningMethodAsync();
    await slowTask.WaitAsync(TimeSpan.FromSeconds(5));
}
catch (TimeoutException)
{
    Console.WriteLine("The task timed out via WaitAsync.");
}

WaitAsync accepts either a TimeSpan or a CancellationToken directly, combining simplicity with optimized resource handling. Use WaitAsync whenever targeting modern .NET runtimes.