Search code examples
c#tasktask-parallel-librarycancellation-tokenc#-12.0

Why the catch block of AggregateException was not sufficient to handle cancellations?


Run the following code:

using static System.Console;
WriteLine("Handling cancellations and exceptions.");
CancellationTokenSource cts = new();
CancellationToken token = cts.Token;

var transferMoney = Task<string>.Factory.StartNew(
 () =>
 {
     WriteLine($"Initiating the money transfer.");
     int progressBar = 0;
     WriteLine("Press c to cancel withing 5 sec.");
     // Assuming the task will take 5 seconds.
     // So, after every second, we'll increase the progress by 20%
     for (int i = 0; i < 5; i++)
     {
         token.ThrowIfCancellationRequested();
         Thread.Sleep(1000);
         progressBar += 20;
         WriteLine($"Progress:{progressBar}%");
     }
     return "Money transfer is completed.";
 }, token);

var input = ReadKey().KeyChar;
if (input.Equals('c'))
{
    WriteLine("\nCancellation is requested.");
    cts.Cancel();
}
try
{

    transferMoney.Wait();   
    WriteLine(transferMoney.Result);
}
catch (AggregateException ae)
{
    ae.Handle(e =>
    {
        WriteLine($"Caught error :  {e.Message}");
        return true;
    });
}

catch (OperationCanceledException oce)
{
    WriteLine($"Caught error due to cancellation :{oce.Message}");
}

WriteLine($"Payment processing status: {transferMoney.Status}");
WriteLine("Thank you, visit again!");

Here is a sample output( expected behavior- no doubt):

Handling cancellations and exceptions.
Initiating the money transfer.
Press c to cancel withing 5 sec.
Progress:20%
Progress:40%
Progress:60%
c
Cancellation is requested.
Progress:80%
Caught error :  A task was canceled.
Payment processing status: Canceled
Thank you, visit again!

Now update the try block with any of the following statement:

try
{
    //transferMoney.Wait();
    transferMoney.Wait(token); 
    //await transferMoney; //Same observation
    // There is no change in the remaining code

And run the code again. Here is a sample output.

Handling cancellations and exceptions.
Initiating the money transfer.
Press c to cancel withing 5 sec.
Progress:20%
Progress:40%
c
Cancellation is requested.
**Caught error due to cancellation :The operation was canceled.**
Payment processing status: Running
Thank you, visit again!

Notice that this time the catch block for AggregateException was not sufficient to handle OperationCanceledException. I'd like to know the reason behind this. Can you please share your thoughts-where am I missing? [Additional note: I understand that in earlier case, wait() can throw only AggregateException but this overloaded version of Wait(i.e. Wait(token)) can throw OperationCancelledException as well. ]


Solution

  • From the Task.Wait(CancellationToken) docs:

    but the wait is canceled once the cancellation token is cancelled and an OperationCanceledException is thrown.

    And

    The Wait(CancellationToken) method creates a cancelable wait; that is, it causes the current thread to wait until one of the following occurs:

    • The task completes.
    • The cancellation token is canceled. In this case, the call to the Wait(CancellationToken) method throws an OperationCanceledException.

    Your wait is cancelled in the second case so you get OperationCanceledException, i.e. the method can understand that the cancellation (of monitored token) has happened, not some random error (which will go to the "task completes" case and will result in AggregateException) .

    OperationCanceledException does not inherit from the AggregateException so the catch(AggregateException) can't handle it.

    OperationCanceledException is prioritized over the any other exception if any happened during the task execution. From the Task.Wait source code:

    // If cancellation was requested and the task was canceled, throw an
    // OperationCanceledException.  This is prioritized ahead of the ThrowIfExceptional
    // call to bring more determinism to cases where the same token is used to
    // cancel the Wait and to cancel the Task.  
    // Otherwise, there's a race condition between
    // whether the Wait or the Task observes the cancellation request first,
    // and different exceptions result from the different cases.
    
    if (IsCanceled) cancellationToken.ThrowIfCancellationRequested();
    
    // If an exception occurred, or the task was cancelled, throw an exception.
    
    ThrowIfExceptional(true);