Search code examples
c#async-awaittaskcontinuations

TaskContinuationOptions OnlyOnCancelled catches unhandled errors


I have following code to handle my TaskContinuations. I am bit confused because I have below OnlyOnFaulted block which I expect will be entered if the task throws an unhandled exception.

However, unhandled exception, handled exception that is rethrown using throw, or cancellation will land in the OnlyOnCanceled block.

GetDataAsync(id).ContinueWith((antecedant) =>
{
    // do something when async method completed
    }, TaskContinuationOptions.OnlyOnRanToCompletion)
    .ContinueWith((antecedant) =>
    {
        var error = antecedant.Exception.Flatten(); //so when is this called if everything is cought by OnCancelled below?
    }, TaskContinuationOptions.OnlyOnFaulted)
    .ContinueWith((antecedant) =>
    {
        // this is fired if method throws an exception or if CancellationToken cancelled it or if unhandled exception cought
        var error = "Task has been cancelled";
    }, TaskContinuationOptions.OnlyOnCanceled);

I would expect that re-thrown errors and cancellations will land in the OnlyOnCanceled block whereas unhandled exception will land in the OnlyOnFaulted block

Note that I cannot just await GetDataAsync because this is called in a method called from View's c-tor. I explained that in this post NetworkStream ReadAsync and WriteAsync hang infinitelly when using CancellationTokenSource - Deadlock Caused by Task.Result (or Task.Wait)

UPDATE

Instead using code above, I am using Task.Run like below. I am decorating the lambda passed into Task.Run with async to provide "Async all the way" as recommended by Jon Goldberger at https://blog.xamarin.com/getting-started-with-async-await/

Task.Run(async() =>  
{
    try
    {
        IList<MyModel> models = await GetDataAsync(id);
        foreach (var model in models)
        {
            MyModelsObservableCollection.Add(model);
        }
    } catch (OperationCancelledException oce) {}
    } catch (Exception ex) {}

});

This felt a better solution since I can wrap the code inside Task.Run with try...catch block and the exception handling is behaving as I would expect.

I am definitely planning to give a try to suggestion offered by Stephen Cleary at https://msdn.microsoft.com/en-us/magazine/dn605875.aspx as it seam to be a cleaner solution.


Solution

  • As I said in my other answer, you should use await, and, since this is a constructor for a ViewModel, you should synchronously initialize to a "Loading..." state and asynchronously update that ViewModel to a "Display Data" state.

    To answer this question directly, the problem is that ContinueWith returns a task representing the continuation, not the antecedent. To simplify the code in your question:

    GetDataAsync(id)
        .ContinueWith(A(), TaskContinuationOptions.OnlyOnRanToCompletion);
        .ContinueWith(B(), TaskContinuationOptions.OnlyOnFaulted)
        .ContinueWith(C(), TaskContinuationOptions.OnlyOnCanceled);
    

    A() will be called if GetDataAsync(id) runs to completion. B() will be called if A() faults (note: not if GetDataAsync(id) faults). C() will be called if B() is canceled (note: not if GetDataAsync(id) is canceled).

    There are a couple of other problems with your usage of ContinueWith: it's missing some flags (e.g., DenyChildAttach), and it's using the current TaskScheduler, which can cause surprising behavior. ContinueWith is an advanced, low-level method; use await instead.