Rick Strahl's Weblog  

Wind, waves, code and everything in between...
.NET • C# • Markdown • WPF • All Things Web
Contact   •   Articles   •   Products   •   Support   •   Advertise
Sponsored by:
West Wind WebSurge - Rest Client and Http Load Testing for Windows

Back to Basics: Await a Task with a Timeout


:P
On this page:

Time Is Running Out

The other day I needed to set a TaskCompletionSource and wait on an operation to complete. Easy enough - you can just await the tcs.Task. But in some cases the operation doesn't complete, and so it's also necessary to allow for a time out to stop waiting and assume the operation failed.

Task natively doesn't have a way to provide for a timeout on a single task, and by default an await call on a task results in an indefinite wait on the task to complete which... can be problematic in cases where the task is expected in some cases to not fire.

Task.WaitAsync() is new in .NET 8.0

Turns out that .NET 8.0 and later introduces Task.WaitAsync() instance method, that provides similar functionality to what I discuss in this post. Thanks to several commentors who pointed this out - I'd missed this new feature. I've added a section for Task.WaitAsync() to the bottom of the post. Keeping the post as it provides support for older versions of .NET including .NET Framework as well as some insight on how this works.

In my scenario this was a WebView navigation which can fail if an invalid URL is provided, or if you're navigating to site that is there, but not connecting and indefinitely hanging. In that scenario you want to have an out so that you are not waiting forever on that task to complete.

Checking for a Task Operation Timeout

So how do 'time out' a Task that you're waiting on? You can check for a timeout by combining Task.Delay() and Task.WhenAny() to return the first task that completes:

var taskToComplete = SomeTaskOperation(); // not awaited here
var timeoutTask = Task.Delay(msTimeout);

var completedTask = await Task.WhenAny(taskToComplete, timeoutTask);
bool completed = completedTask == taskToComplete;

The idea is that you run both your task and the delay task and if our task completes first then it didn't time out. If the delay completes first, your task took too long. Times up, lights out!

Turn it into an Extension Method

To make this more generic we could create a couple of extension methods, one that simply checks for a timeout and another that returns the result of both the timeout status and the result value:

public static class TaskExtensions
{
    public static async Task<bool> Timeout(this Task task, int timeoutMs)
    {
        var completed = await Task.WhenAny(task, Task.Delay(timeoutMs));
        
        if (task.IsFaulted)
            throw task.Exception.GetBaseException();

        return completed == task && task.IsCompleted;
    }
}

To use this first one, you check for the timeout to succeed and then pick up any result value from task.Result of the now completed task:

var taskToComplete = DoSomething(1000);  // non-awaited async method below

// Call and retrieve success or timeout
bool isComplete = await taskToComplete.Timeout(2000);

// should time out after 2 seconds
Assert.IsTrue(isComplete, "Task did not complete in time");

// if sucess get the Result from the task
int result = taskToComplete.Result;
Assert.AreEqual(result, 100);
---

private async Task<int> DoSomething(int timeout)
{
    await Task.Delay(timeout);
    return 100;
}    

In this code I want to execute a task that takes 1000ms - so it succeeds as it's under the 2000ms timeout. If the task takes 3000ms then it fails as it's longer than the 2000ms timeout.

If you prefer to retrieve the result, but deal with exceptions instead, you can use the TimeoutWithResult() method instead, which directly returns a result and throws an exception if the operation times out.

public static async Task<TResult> TimeoutWithResult<TResult>(this Task<TResult> task, int timeoutMs)
{                       
    var completed = await Task.WhenAny(task, Task.Delay(timeoutMs));     
    
    if (task.IsFaulted)
        throw task.Exception.GetBaseException();

    if (completed == task && task.IsCompleted)
    {
        return task.Result;
    }

    throw new TimeoutException();
}

You can call this method like this:

int result = 0;
try
{
    // no timeout no exception
    result = await DoSomething(1500).TimeoutWithResult(2000);
}
catch(TimeoutException ex)
{
    Assert.Fail("Task should not have timed out: " + ex.Message);
}
catch(Exception ex)
{
    Assert.Fail("Task should not have thrown an exception: " + ex.Message);
}

// No timeout since task completes in 1.5 seconds  
Assert.IsTrue(result==100, "Task did not return the correct result");

This method has some limitations in that the default() result can mean either the method call returned a default() result or the call timed out. If you don't care about the why it 'failed' then this method is a little cleaner to use.

Note that in both methods errors are propagated so you can use regular try/catch to catch exceptions in task code to catch failures in the executed task.

Using Task.WaitAsync()

As mentioned by several commentors, as of .NET 8.0 there's a new Task.WaitAsync() instance method that can be used to wait for completion. The method is similar to the TimeoutWithResult<TResult>() method I've described, but it throws exceptions when timed out, so the behavior is slightly different:

var taskToComplete = DoSomething(1000);

bool isComplete = false;
int result = 0;
try
{
    result = await taskToComplete.WaitAsync(TimeSpan.FromSeconds(2));
}
// Capture Timeout
catch(TimeoutException ex)
{
    Console.WriteLine("Task timed out: " + ex.Message);
}
// Fires on Exception in taskToComplete
catch (Exception ex)
{
    Console.WriteLine("Error: " + ex.Message);
}

Assert.AreEqual(100, result);

The method works similar to the described method here, by waiting for completion or simply returning at which point you can check the the task.IsCompleted

Summary

This is not anything new and well, there's now a native method that provides this functionality, but I've run into cases where I needed a timeout on the async code I'm calling in a semi-generic way. It'd be nice if there was native support for this, but the code above is easy enough to integrate into a small helper library if necessary.

If you're not using .NET 8.0 yet or still using .NET Framework these helper functions will work for you in those environments.

I also added these functions into Westwind.Utilities in the AsyncUtils class as extension methods to Task.

Posted in .NET  

The Voices of Reason


 

Andrew Lock
July 26, 2024

# re: Back to Basics: Await a Task with a Timeout

Came to point out WaitAsync() but I guess someone got there first ๐Ÿ˜„ it was actually introduced in .net 6, so more widely available๐Ÿ˜Š I wrote a post about it and how it's implemented here: https://andrewlock.net/a-deep-dive-into-the-new-task-waitasync-api-in-dotnet-6/

Maybe also worth pointing out that just because you stopped waiting for the task, doesn't mean it stops running on the background: https://andrewlock.net/just-because-you-stopped-waiting-for-it-doesnt-mean-the-task-stopped-running/


Jesse Slicer
July 28, 2024

# re: Back to Basics: Await a Task with a Timeout

This is a little more old school (C# 7, Framework 4.6)... shouldn't this do similarly?

using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(1.5)))
{
    try
    {
        await SomeTaskOperation(cts.Token).ConfigureAwait(false);
    }
    catch (OperationCanceledException ex) when (cts.IsCancellationRequested)
    {
        // Timeout occurred, do what you might need to do.
    }
}

private async Task SomeOperation(CancellationToken token = default(CancellationToken))
{
    while (true)
    {
        if (cts.IsCancellationRequested)
        {
            throw new OperationCanceledException();
        }

        // Do something async.
    }

}

Aurelien Dellieux
July 28, 2024

William Kempf
July 29, 2024

# re: Back to Basics: Await a Task with a Timeout

@Andrew, the corollary is true as well... just because you don't wait for a task does not mean it completes. There's huge potential for bugs with "fire and forget" tasks. If a task is running and the process ends, the "task" will be prematurely ended, likely in the middle of some critical operation such as writing to disk, corrupting state. All tasks should be "observed" to complete, always.

It's always been possible to implement a timeout using a CancellationTokenSource, Task.WaitAsync just makes it easier. https://learn.microsoft.com/en-us/dotnet/csharp/asynchronous-programming/cancel-async-tasks-after-a-period-of-time


Rick Strahl
July 29, 2024

# re: Back to Basics: Await a Task with a Timeout

@William - I'm not sure that an absolute 'All Tasks need to be observed' is a necessary requirement.

While I'm with you on making sure that any critical tasks that can potentially cause instability or lock up should be observed, surely letting a Task.Delay() run without completion is not going to break anything. Likewise waiting on a UI operation to complete that may never complete is not something you can await indefinitely, especially since most of those don't have a CancellationToken that you can cancel on. Preciously few operations outside of the core framework offer CancellationToken support, likely because it's an awkward implementation.

FWIW, in most of my applications I use a .FireAndForget()` method to 'await' tasks and ignore the results when not explicitly awaiting in mainline code.


William Kempf
July 30, 2024

# re: Back to Basics: Await a Task with a Timeout

@Rick, what's the point of a Task.Delay if you don't observe it? I take your point, though. There are some operations that simply won't cause problems if you terminate them in the middle of processing. However, I will say it's bad design to create and not observe such tasks (there's a reason we have cooperative cancellation and why things like Thread.Abort are considered dangerous). Can you get away with such code? Sure. Is it a good idea? In general, NO. I shouldn't speak in absolutes, but in this case I do so for a reason. I've seen far too many cases of "fire and forget" that can be disastrous when the application shuts down for any reason. Rather than talk about the nuances, corner cases and alternative designs, it's easier to talk in absolutes.


Rick Strahl
July 30, 2024

# re: Back to Basics: Await a Task with a Timeout

@William - Did you not read the post? ๐Ÿ˜„ The timeout uses a Task.Delay with WhenAny() to time out. If timed out before the delay completes the Task.Delay() keeps running.

I think we agree on the principle of trying to make tasks observed. I just hate absolutes 'thou shalt not do' because they are rarely appropriate for all scenarios. As you point out the better to make sure all tasks are somehow awaited/continued and to use something like .FireAndForget() style continuations to ensure that non-observed tasks runs to completion and that exceptions are properly terminated.


William Kempf
July 30, 2024

# re: Back to Basics: Await a Task with a Timeout

I did read the article. WhenAny is a form of observation. Granted, you didn't cancel the delay (wasting resources) or ensure the completion was observed, but you did observe the delay. Not observing it at all would be pointless, no?

But, we're both being pedantic. I think we both mostly agree with the other. I'll continue to be hyperbolic, as I think it's safer, even if not correct. ๐Ÿ˜ƒ


West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2024