Search code examples
c#loggingtask-parallel-libraryasync-awaitvisual-studio-debugging

Why is only one from many exceptions from child tasks always propagated?


I am struggling to better grasp the rationale of exception and error handling in TPL (and with some more luck in .NET 4.5 async/await tasks)

The slightly modified from my earlier question "How to better understand the code/statements from "Async - Handling multiple Exceptions" article?" C# console app code running 2 detached inner nested attached (dependent) child (Update: sorry, started one question but ended by another!) tasks:

class Program
{  
   static void Main(string[] args)
   {  Tst();
      Console.ReadLine();
   }
   async static Task  Tst()
   {
       try
       {
           await Task.Factory.StartNew
             (() =>
                {
                   Task.Factory.StartNew
                       (   () => { 
                                    Console.WriteLine("From 1st child");
                                    throw new NullReferenceException(); 
                                  }
                            , TaskCreationOptions.AttachedToParent
                        );
               Task.Factory.StartNew
                       (  () =>
                               { 
                                   Console.WriteLine("From 2nd child");
                                   throw new ArgumentException(); 
                               }
      ,TaskCreationOptions.AttachedToParent
                       );
                }
             );
    }
    catch (AggregateException ex)
    {
        Console.WriteLine("** {0} **", ex.GetType().Name);
        foreach (var exc in ex.Flatten().InnerExceptions)
        {
             Console.WriteLine(exc.GetType().Name);
        }
    }
    catch (Exception ex)
    {
       Console.WriteLine("## {0} ##", ex.GetType().Name);
    }
 } 

produces output that alternates (non-deterministically) between:

From 1st child
From 2nd child
** AggregateException **
ArgumentException

and

From 1t child
From 2nd child
** AggregateException **
NullReferenceException

Seems like always one and only one exception from one of a child tasks always propagated/caught.

Why is only one exception propagated/caught?
I'd have better understood if none or rather all exceptions from child tasks are always caught

Is it possible, in this situation, that both or none exception will be caught?


Solution

  • You should not mix parent/child tasks with async. They were not designed to go together.

    svick already answered this question as part of his (correct) answer to your other question. Here's how you can think of it:

    • Each inner StartNew gets one exception, which is wrapped into an AggregateException and placed on the returned Task.
    • The outer StartNew gets both AggregateExceptions from its child tasks, which it wraps into another AggregateException on its returned Task.
    • When you await a Task, the first inner exception is raised. Any others are ignored.

    You can observe this behavior by saving the Tasks and inspecting them after the exception is raised by await:

    async static Task Test()
    {
        Task containingTask, nullRefTask, argTask;
        try
        {
            containingTask = Task.Factory.StartNew(() =>
            {
                nullRefTask = Task.Factory.StartNew(() =>
                {
                    throw new NullReferenceException();
                }, TaskCreationOptions.AttachedToParent);
                argTask = Task.Factory.StartNew(() =>
                {
                    throw new ArgumentException();
                }, TaskCreationOptions.AttachedToParent);
            });
            await containingTask;
        }
        catch (AggregateException ex)
        {
            Console.WriteLine("** {0} **", ex.GetType().Name);
        }
    }
    

    If you put a breakpoint on WriteLine, you can see that the exceptions from both child tasks are being placed on the parent task. The await operator only propagates one of them, so that's why you only catch one.