Michael Michael - 12 days ago 5
C# Question

Awaited task ended in Canceled state does not throw

Reading Task-based Asynchronous Pattern by Stephen Toub I'm trying to see how cancellation works for tasks.
In the section Consuming the Task-based Asynchronous Pattern under Await, in 3-rd paragraph it says:


If the Task or Task TResult> awaited ended in the Canceled state, an
OperationCanceledException will be thrown.


I'm trying to see this in action in the code below.

static void Main(string[] args)
{
CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
CancellationToken cancellationToken = cancellationTokenSource.Token;

Task<int> valueTask = DoStuffAsync(cancellationToken);
Thread.Sleep(TimeSpan.FromSeconds(1));
cancellationTokenSource.Cancel();

Console.WriteLine("value task's status: {0}", valueTask.Status);
Console.ReadLine();
}


And the
DoStuffAsync()
method

static async Task<int> DoStuffAsync(CancellationToken cancellationToken)
{
await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);
return 42;
}


Executing this code doesn't throw any exception, it just prints:


value task's status: Cancelled


Now, my expectation was in
DoStuffAsync()
method since
await Task.Delay(...)
is cancelled, we have awaited task ended in Canceled state, hence exception should have been thrown (according to quote from the TAP document), but if I put a breakpoint on
Console.ReadLine()
and check
valueTask
it's Status is Cancelled, and Exception is
null
.

Can anybody help me understand if I misread the document, or code I come up with is not properly reproducing the case?

Answer

The method returns the task itself and the result itself is never accessed. If you would try to access valueTask.Result, you would get a TaskCanceledException (inside an AggregateException).

Likewise, if you would await valueTask (Main would have to by async), you would try to obtain the result in which case the exception would also be thrown. This is the behaviour described in the mentioned paragraph.

The clue is that the Task object is valid, but the result is not because the code after await is never executed if the task is cancelled. For example with:

static async Task<int> DoStuffAsync(CancellationToken cancellationToken)
{
    var delay = Task.Delay(5000, cancellationToken);
    Console.WriteLine("before await"); 
    await delay;
    Console.WriteLine("after await"); 
    return 42;
}

The second writeline is never executed if the task is cancelled. As long as no result is accessed of the task returned by DoStuffAsync, the task is a valid object, just cancelled. Accessing the result will force the runtime to acknowledge that the task was never finished and throw an exception.

If the async method does not return a task, you would also get a TaskCancelled Exception:

static async void Main()
{
   CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
   CancellationToken cancellationToken = cancellationTokenSource.Token;

   DoStuffAsync(cancellationToken);
   Thread.Sleep(TimeSpan.FromSeconds(1));
   cancellationTokenSource.Cancel();
   Console.ReadLine();
}

// Define other methods and classes here
static async void DoStuffAsync(CancellationToken cancellationToken)
{
    await Task.Delay(5000, cancellationToken);
}

Because there is no task object to be returned which can contain the status, the compiler has to warn the executing code that it could not completely run the asynchronous code and an exception is thrown.

Comments