Timeout pattern on task-based asynchronous method in C#

While you can reuse WithCancellation for both cancellations and timeouts I think it's an overkill for what you need.

A simpler and clearer solution for an async operation timeout would be to await both the actual operation and a timeout task using Task.WhenAny. If the timeout task completes first, you got yourself a timeout. Otherwise, the operation completed successfully:

public static async Task<TResult> WithTimeout<TResult>(this Task<TResult> task, TimeSpan timeout)
{
    if (task == await Task.WhenAny(task, Task.Delay(timeout)))
    {
        return await task;
    }
    throw new TimeoutException();
}

Usage:

try
{
    await DoStuffAsync().WithTimeout(TimeSpan.FromSeconds(5));
}
catch (TimeoutException)
{
    // Handle timeout.
}

If you prefer to not throw an exception (as I do) it's even simpler, just return the default value:

public static Task<TResult> WithTimeout<TResult>(this Task<TResult> task, TimeSpan timeout)
{
    var timeoutTask = Task.Delay(timeout).ContinueWith(_ => default(TResult), TaskContinuationOptions.ExecuteSynchronously);
    return Task.WhenAny(task, timeoutTask).Unwrap();
}

Is there any pattern that I'm missing or, am I in the right way if I develop APIs using the no built-in timeout?

Disclaimer:

When we talk about a Task in a cancelled state, we mean that we cancel the operation as it proceeds. This might not be the case here when we talk about cancellation, as we simply discard the task if it completed after the specified interval. This is discussed to extent in Stephan Toubs article below as to why the BCL does not provide OOTB features of cancelling an ongoing operation.


The common approach i see nowadays is the no build-in approach and the one i find myself using mostly to implement a cancelling mechanism. It is definitely the easier of the two, leaving the highest frame to be in charge of cancellation while passing the inner frames the cancellation token. If you find yourself repeating this pattern, you can use the known WithCancellation extension method:

public static async Task<T> WithCancellation<T>(
    this Task<T> task, CancellationToken cancellationToken)
{
    var cancellationCompletionSource = new TaskCompletionSource<bool>();

    using (cancellationToken.Register(() => cancellationCompletionSource.TrySetResult(true)))
    {
        if (task != await Task.WhenAny(task, cancellationCompletionSource.Task))
        {
            throw new OperationCanceledException(cancellationToken);
        }
    }

    return await task;
}

This is from Stephen Toub's How do I cancel non-cancelable async operations? which isn't exactly spot on to what you're asking, but is definitely worth a read.

The Task Cancellation docs go on to specify two ways of task cancellation:

You can terminate the operation by using one of these options:

  1. By simply returning from the delegate. In many scenarios this is sufficient; however, a task instance that is canceled in this way transitions to the TaskStatus.RanToCompletion state, not to the TaskStatus.Canceled state.

  2. By throwing a OperationCanceledException and passing it the token on which cancellation was requested. The preferred way to do this is to use the ThrowIfCancellationRequested method. A task that is canceled in this way transitions to the Canceled state, which the calling code can use to verify that the task responded to its cancellation request

Edit

As for you concern with using a TimeSpan to specify the desired interval, use the overload of CancellationTokenSource constructor which takes a TimeSpan parameter:

var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3));

var task = Task.Run(() => DoStuff()).WithCancellation(cts.Token);