WaitAsync

.NET 6 has a new method added to System.Threading.Tasks.Task to wait for a task completion or timeout. WaitAsync method completes with the result of the task it is invoked on, or throws a timeout exception when the given timeout reached or throws a TaskCancelledException if the given cancellation token is set to cancelled state.

The method's primary use-case is to add cancellation or timeout ability for async methods, that inherently don't provide such capability. One use-case could be unit tests, where we explicitly want to time out and fail a test if the method invoked as the system under test is an async operation without a timeout overload. This way we can let other tests to be executed in the test suite.

Previously a poor man (woman)'s implementation for such timeout could look as:

public static async Task<T> TimeoutAfter<T>(this Task<T> task, TimeSpan timeout)
{
    if (task.IsCompleted)
        return await task;
    var cts = new CancellationTokenSource();
    if (task == await Task.WhenAny(task, Task.Delay(timeout, cts.Token)))
    {
        cts.Cancel();
        return await task;
    }
    else
    {
        throw new TimeoutException();
    }
}

In the above example, a developer has to take care of the extra cancellation of the CancellationTokenSource, even if the task completes successfully. Otherwise for non-timeout cases it could leave the delay task still being executed until the timeout expires. Having many non-expired timeout's concurrently would degrade the performance of the timer that is used by the Task.Delay. Hence it is suggested to proactively cancel the delay task.

The above code becomes now legacy. The new WaitAsync API may replace it, which has three overloads, with the most complex signature:

Task WaitAsync(TimeSpan timeout, CancellationToken cancellationToken);

The API methods has three overloads:

  • one that takes an input parameter TimeSpan for the timeout time

  • one that takes a CancellationToken

  • one that takes both of them

Note, that the API returns the underlying task if it completes synchronously, otherwise it may return a new task with the result of the underlying task. Hence we shall use reference equality checks on the returned task object.

An Example

For example can use the above WaitAsync method with a timespan overload:

using System;
using System.Threading.Tasks;

async Task LongRunningWorkAsync()
{
    while (true)
        await Task.Delay(100);
}

await LongRunningWorkAsync().WaitAsync(TimeSpan.FromSeconds(2));

LongRunningWorkAsync() is being awaited with a timeout of 2 seconds. Because the await method never returns, the application will throw a timeout exception after 2 seconds.

Negative Infinity

An interesting aspect of the API is using it with -1 milliseconds as the timeout period. This value has a special meaning of negative infinity. That means the WaitAsync method will disregard the timeout parameter, and only completes if the awaited task compeletes or cancellation triggered.

CancellationTokenSource cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(2));
await LongRunningWorkAsync().WaitAsync(TimeSpan.FromMilliseconds(-1), cts.Token);

The code above would throw a TaskCanceledException after two seconds.