I'm doing a code review, and I'm concerned about this pattern, seen all across that code:
try
{
await DoSomethingAsync();
await DoSomethingElseAsync();
// and so on...
}
catch (OperationCanceledException)
{
// all good, user cancelled
// log and return
return;
}
// handle other particular exceptions
// ...
catch (Exception ex)
{
// fatal error, alert the user
FatalErrorMessage(ex);
}
The part I'm concerned about is handling OperationCanceledException
. Shouldn't this code be also handling AggregateException
and checking if the only inner exception is OperationCanceledException
?
I know Task.Wait
or Task.Result
would throw an AggregateException
like that, rather than OperationCanceledException
. The author of the code assured me she only uses async/await
inside out and never uses Wait/Result
. Thus, she doesn't like the idea of additionally observing AggregateException
for cancellation. However, my point is some standard Task
-based BCL APIs could still be wrapping OperationCanceledException
with AggregateException
, e.g. because they still might be accessing Task.Result
internally.
Does it make sense? Should we be worrying about handling both OperationCanceledException
and AggregateException
to observe cancellation correctly?
Well, it's definitely technically possible which is very easy to verify with this code:
static void Main()
{
Test().Wait();
}
static async Task Test()
{
try
{
await ThrowAggregate();
}
catch (Exception e)
{
Console.WriteLine(e);
}
}
static async Task ThrowAggregate()
{
ThrowException().Wait();
}
static async Task ThrowException()
{
throw new OperationCanceledException();
}
ThrowAggregate
stores the AggregateException inside the returned task so awaiting it still throws AggregateException
. So if you want to be diligent you would need to catch AggregateException
too.
However, it's very unlikely that any method in the BCL would do that and if it did you have bigger issues than exception handling since your doing async over sync. I would be more worried about your own code.