TaskCanceledException when calling Task.Delay with a CancellationToken in an keyboard event

12,647

Solution 1

That's to be expected. When you cancel the old Delay, it will raise an exception; that's how cancellation works. You can put a simple try/catch around the Delay to catch the expected exception.

Note that if you want to do time-based logic like this, Rx is a more natural fit than async.

Solution 2

If you add ContinueWith() with an empty action, the exception isn't thrown. The exception is caught and passed to the tsk.Exception property in the ContinueWith(). But It saves you from writing a try/catch that uglifies your code.

await Task.Delay(500, cancellationToken.Token).ContinueWith(tsk => { });

UPDATE:

Instead of writing code to handle the exception, a boolean would be much cleaner. This is only preferred when a delay cancel is expected!. One way is to create a helper class (Although I don't like helper classes much)

namespace System.Threading.Tasks
{
    public static class TaskDelay
    {
        public static Task<bool> Wait(TimeSpan timeout, CancellationToken token) =>
            Task.Delay(timeout, token).ContinueWith(tsk => tsk.Exception == default);

        public static Task<bool> Wait(int timeoutMs, CancellationToken token) =>
            Task.Delay(timeoutMs, token).ContinueWith(tsk => tsk.Exception == default);
    }
}

For example:

var source = new CancellationTokenSource();

if(!await TaskDelay.Wait(2000, source.Token))
{
    // The Delay task was canceled.
}

(don't forget to dispose the source)

Solution 3

Curiously, the cancellation exception seems to only be thrown when the cancellation token is on Task.Delay. Put the token on the ContinueWith and no cancel exception is thrown:

Task.Delay(500).ContinueWith(tsk => {
   //code to run after the delay goes here
}, cancellationToken.Token);

You can just chain on yet another .ContinueWith() if you really want to catch any cancellation exception - it'll be passed into there.

Solution 4

Another easy way to suppress the exception of an awaited task is to pass the task as a single argument to Task.WhenAny:

Creates a task that will complete when any of the supplied tasks have completed.

await Task.WhenAny(Task.Delay(500, token)); // Ignores the exception

One problem of this approach is that it doesn't communicate clearly its intention, so adding a comment is recommended. Another one is that it results in an allocation (because of the params in the signature of Task.WhenAny).

Solution 5

I think it deserves to add a comment about why it works that way.

The doc is actually wrong or written unclear about the TaskCancelledException for Task.Delay method. The Delay method itself never throws that exception. It transfers the task into cancelled state, and what exactly raises the exception is await. It didn't matter here that Task.Delay method is used. It would work the same way with any other cancelled task, this is how cancellation is expected to work. And this actually explains why adding a continuation mysteriously hides the exception. Because it's caused by await.

Share:
12,647

Related videos on Youtube

Andrew Roberts
Author by

Andrew Roberts

Updated on June 04, 2022

Comments

  • Andrew Roberts
    Andrew Roberts almost 2 years

    I am trying to delay the processing of a method (SubmitQuery() in the example) called from an keyboard event in WinRT until there has been no further events for a time period (500ms in this case).

    I only want SubmitQuery() to run when I think the user has finished typing.

    Using the code below, I keep getting a System.Threading.Tasks.TaskCanceledException when Task.Delay(500, cancellationToken.Token); is called. What am I doing wrong here please?

    CancellationTokenSource cancellationToken = new CancellationTokenSource();
    
    private async void SearchBox_QueryChanged(SearchBox sender, SearchBoxQueryChangedEventArgs args)
    {
    
            cancellationToken.Cancel();
            cancellationToken = new CancellationTokenSource();
    
        await Task.Delay(500, cancellationToken.Token);
    
        if (!cancellationToken.IsCancellationRequested)
        {
            await ViewModel.SubmitQuery();
        }
    }
    
  • Anupam
    Anupam over 10 years
    could you please provide link to some resources on how Rx is better fit for time-based logic
  • Stephen Cleary
    Stephen Cleary over 10 years
    @Anupam: That's from my own experience.