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. ]
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 anOperationCanceledException
.
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);