Search code examples
c#.nettask-parallel-libraryasync-awaitcancellation

General approach to handle Task cancellation correctly


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?


Solution

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