27

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();
    }
}
i3arnon
  • 113,022
  • 33
  • 324
  • 344
Andrew Roberts
  • 990
  • 2
  • 12
  • 26

6 Answers6

68

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)

Jeroen van Langen
  • 21,446
  • 3
  • 42
  • 57
  • 6
    That's awesome, thanks. Saves some really ugly try/catch code :) – René Sackers Apr 23 '17 at 20:20
  • 8
    Thanks for this! Very annoying in the debugger even though it is caught. I think in these `.Delay` situations we **expect** the token to be cancelled so the exception shouldn't even raised/caught, and this fixed that perfectly. – Andrew Sep 26 '17 at 04:30
  • Really awesome, but why the exception isn't thrown in this case @j-van-langen? – Alexsandro Jan 20 '19 at 02:01
  • 2
    @Alexsandro That's because it is catched and the exception is passed in the `Task tsk` argument. `.ContinueWith(tsk => tsk.Exception);` This way the continued task can handle the exception. In this case we don't.... – Jeroen van Langen Jan 20 '19 at 21:34
  • 2
    I think it should be noted that the ContinueWith method does catch and absorb the exception, but any code after task.delay is then still executed. So if you are calling this inside of an async Task method, this is probably not a good idea as the point of the cancellation token is to cancel the current task. In this case you will have to manually check the cancellation token for IsCancellationRequested or just leave the try/catch there.. – Quintonn Mar 05 '19 at 16:29
  • @Alexsandro "but why the exception isn't thrown in this case?" - I have added another answer explaining why it happens. – lilo0 Mar 09 '21 at 07:31
  • Is it really so ugly? `try { await Task.Delay(500, cancellationToken.Token); } catch { }` I don't think so. It looks concise and expressive to me. – Theodor Zoulias May 11 '21 at 10:03
  • @TheodorZoulias yes, I do think so. I'd rather have a boolean to check if it was completed or not. (I'll add an idea) – Jeroen van Langen May 11 '21 at 14:08
  • 1
    Be aware that the `ContinueWith` method is characterized as [dangerous](https://blog.stephencleary.com/2013/10/continuewith-is-dangerous-too.html) by Stephen Cleary, unless the `TaskScheduler` argument is explicitly specified. – Theodor Zoulias May 11 '21 at 16:17
  • @TheodorZoulias Thanks for pointing that out. I'll read it. – Jeroen van Langen May 11 '21 at 20:57
  • Isn't it using the TaskScheduler which the Task.Delay is using? – Jeroen van Langen May 11 '21 at 20:59
  • 1
    AFAIK no, unless you specify the `TaskContinuationOptions.ExecuteSynchronously` option. By default the `ContinueWith` task is scheduled independently from the antecedent task. – Theodor Zoulias May 11 '21 at 21:21
  • 1
    Btw a few months ago Microsoft was contemplating the idea of offering [more configuration options](https://github.com/dotnet/runtime/issues/47525 "Developers can have access to more options when configuring async awaitables") for `await`, including a no-throw option. Initially the suggestion was `.ConfigureAwait(AwaitBehavior.NoThrow)`, then became `.ConfigureAwait(new AwaitBehavior { SuppressExceptions = true })`, and finally it was scraped altogether with the rationale (I think) that it doesn't add enough value to justify the expansion of the API surface. – Theodor Zoulias May 11 '21 at 21:40
  • It appears to me that the exception is not thrown. It is just created and passed inside the task. Unless `await` sees that exception, it is not thrown. `ContinueWith` swallows the exception and therefore `await` does not see the exception. I have no exception messages in my debugger panel. See also https://stackoverflow.com/a/66433893/193017 – Roland Pihlakas Feb 03 '22 at 19:47
25

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.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
6

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).


Note: When a Task is faulted and its Exception in not observed directly or indirectly, the TaskScheduler.UnobservedTaskException event is triggered for this task, at some point in the future (non-deterministically). The trick shown above, that uses the Task.WhenAny method, does not observe the Exception of the task, so the event will be triggered. In the specific case of the Task.Delay(500, token), the task completes as canceled, not as faulted, so it doesn't trigger the event anyway.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • 1
    Honestly the most elegant solution – SoulKa Aug 04 '22 at 21:49
  • Warning : This did not work anymore with Net7.0 – Florian J Nov 11 '22 at 13:55
  • @FlorianJ I just tested it on .NET 7 with Visual Studio 2022 version 17.4.0, and it works as expected. Also no compiler warnings are generated. Could you describe how exactly is not working for you? – Theodor Zoulias Nov 13 '22 at 09:37
  • @TheodorZoulias you right, Task.WhenAny work as expected, however, using Task.WhenAll throw when token is cancelled. i used Task.WhenAll in my test. – Florian J Nov 14 '22 at 20:07
  • @FlorianJ the `Task.WhenAny` and the `Task.WhenAll` are completely different animals! The one returns a `Task`, and the other returns a `Task`. – Theodor Zoulias Nov 14 '22 at 20:40
  • hmm, no one return a task, one is void, the other is int, i don't understand what you mean – Florian J Nov 14 '22 at 21:12
  • 1
    @FlorianJ now you are probably looking at the `Task.WaitAny` and the `Task.WaitAll`. These are also completely different animals. "Wait" is not the same as "When"! – Theodor Zoulias Nov 14 '22 at 21:24
  • ahah that's right sorry. Btw, Task.WhenAll throw an exception – Florian J Nov 14 '22 at 22:29
  • @FlorianJ yes, the `Task.WhenAll` propagates the exceptions of the tasks. The `Task.WhenAny` never propagates an exception directly. In case the completed task has failed, you must `await` this task in a second step in order to observe the exception. – Theodor Zoulias Nov 14 '22 at 22:57
  • This is a very clean solution, but perhaps has one problem. There are other exceptions that might be thrown, and this ignores them all. Another option is to just catch the TaskCanceledException, log it if you wish, and then carry on. – Robert Wilkinson Nov 16 '22 at 17:19
  • @RobertWilkinson yes, this solution suppresses all exceptions. In the case of the `Task.Delay` there is actually no exception. The `Task` completes in a canceled state, not faulted. There is nothing stored in its [`Exception`](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.exception) property. Nevertheless this state is surfaced as an exception if you `await` the task directly. Switching to `try`/`catch` is a valid option, but comes with [a cost](https://stackoverflow.com/questions/891217/how-expensive-are-exceptions-in-c "How expensive are exceptions in C#?"). – Theodor Zoulias Nov 16 '22 at 17:40
4

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.

lilo0
  • 895
  • 9
  • 12
1

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.

Collie
  • 718
  • 7
  • 6
-1

If you don't care about any exception that may be thrown by the awaited Task you can create an extension method like this:

public static class TaskExtensions
{
    public static async Task<bool> TryAsync(this Task task)
    {
        try
        {
            await task;
            return true;
        }
        catch
        {
            return false;
        }
    }
}

And then use it like this:

var wasCompleted = await Task.Delay(500, cancellationToken).TryAsync();
if (wasCompleted)
{
    // do something
}

Additional

As a compliment you could add a similar extension method for Task<T> to the TaskExtensions class.

public static async Task<(bool wasCompleted, T result)> TryAsync<T>(this Task<T> task)
{
    try
    {
        var result = await task;
        return (true, result);
    }
    catch
    {
        return (false, default(T));
    }
}

Usage:

var (wasCompleted, result) = await GetSomethingAsync().TryAsync();
if (wasCompleted)
{
    var x = result.X;
}
Michael A.
  • 1
  • 1
  • 1