42

I'm reading through a book about the C# Task Parallel Library and have the following example but the TaskScheduler.UnobservedTaskException handler is never being triggered. Can anyone give me any clues as to why?

TaskScheduler.UnobservedTaskException += (object sender, UnobservedTaskExceptionEventArgs eventArgs) =>
{
    eventArgs.SetObserved();
    ((AggregateException)eventArgs.Exception).Handle(ex =>
    {
        Console.WriteLine("Exception type: {0}", ex.GetType());
        return true;
    });
};

Task task1 = new Task(() => 
{
    throw new ArgumentNullException();
});

Task task2 = new Task(() => {
    throw new ArgumentOutOfRangeException();
});

task1.Start();
task2.Start();

while (!task1.IsCompleted || !task2.IsCompleted)
{
    Thread.Sleep( 5000 );
}

Console.WriteLine("done");
Console.ReadLine();
Drew Noakes
  • 300,895
  • 165
  • 679
  • 742
devlife
  • 15,275
  • 27
  • 77
  • 131
  • I'd be really curious, as well - the example is incorrect, since it's impossible for the event to be raised in this example... – Reed Copsey Jul 19 '10 at 19:26
  • 1
    This is the book: http://www.apress.com/book/view/1430229675 Pro .NET 4 Parallel Programming in C# – devlife Jul 20 '10 at 13:48
  • @devlife Hi, im keeping references but my UnobservedTaskException event isnt being fired too here : http://stackoverflow.com/questions/11831844/unobservedtaskexception-being-throw-but-it-is-handled-by-a-taskscheduler-unobser do you found some solution ? – newway Aug 07 '12 at 19:18

3 Answers3

66

Unfortunately, that example will never show you your code. The UnobservedTaskException will only happen if a Task gets collected by the GC with an exception unobserved - as long as you hold a reference to task1 and task2, the GC will never collect, and you'll never see your exception handler.

In order to see the behavior of the UnobservedTaskException in action, I'd try the following (contrived example):

public static void Main()
{
    TaskScheduler.UnobservedTaskException
        +=
        (object? sender, UnobservedTaskExceptionEventArgs eventArgs) =>
            {
                eventArgs.SetObserved();
                ((AggregateException)eventArgs.Exception).Handle(ex =>
                {
                    Console.WriteLine("Exception type: {0}", ex.GetType());
                    return true;
                });
            };

    StartTasks();

    Thread.Sleep(100);
    GC.Collect();
    GC.WaitForPendingFinalizers();

    Console.WriteLine("Done");
    Console.ReadKey();
}

private static void StartTasks()
{
    Task.Factory.StartNew(() => { throw new ArgumentNullException(); });
    Task.Factory.StartNew(() => { throw new ArgumentOutOfRangeException(); });
}

This will show you your messages. The first Thread.Sleep(100) call provides enough time for the tasks to throw. The collect and wait forces a GC collection, which will fire your event handler 2x.

voidp
  • 55
  • 6
Reed Copsey
  • 554,122
  • 78
  • 1,158
  • 1,373
  • 10
    In .NET 4.5, you need extra voodoo for this to happen. First, you need to enable [ThrowUnobservedTaskException](http://msdn.microsoft.com/en-GB/library/jj160346.aspx) in the app.config, and second, you need to build the code in the Release configuration (a point that's only documented in the Example section of the linked MSDN article). I blame this "let's suppress exceptions silently" mentality on JavaScript in browsers :) – Roman Starkov Apr 30 '13 at 14:05
  • 3
    @romkyns, I didn't need to do this for my .NET 4.5 application. – Drew Noakes Jun 17 '15 at 10:07
  • 12
    ThrowUnobservedTaskException is not required for this to work. It is only necessary if you want to revert to the 4.0 behavior of a Task exception terminating the process. In 4.5 the default changed to not terminate the process. It is not required for the unobserved exception to be caught by the event handler. More info in the remarks [here](https://msdn.microsoft.com/en-us/library/system.threading.tasks.taskscheduler.unobservedtaskexception(v=vs.110).aspx) – BitMask777 Sep 21 '15 at 22:48
  • 2
    I tried this code in Core 3.0. It does not trigger any events. Either something is missing in this code or the whole app needs additional configuration/settings. Or maybe this mechanism works only sometimes. – Kirill Kobelev Jan 23 '20 at 03:49
  • 1
    @RomanStarkov funnily enough Node.js has an "always throw" mentality, C# is surprising to me in its silencing approach coming from a JS background! – Marcos Pereira May 05 '22 at 23:54
  • 1
    @MarcosPereira C# had it too, before 4.5. It didn't work. Even in code where we tried really hard to ensure all task exceptions were observed, we got "random" process kills. It doesn't help that it doesn't need to happen in _your_ code, and that you can't even use something as simple as `Task.WhenAll(task1, task2)`, or even `await task1; await task2;` - that already gives opportunities for unobserved exceptions. The end result is that you create far more problems than you solve. – Luaan May 06 '22 at 09:20
4

The exception won't be "unobserved" in that sample snippet. Not until the garbage collector gets rid of the Task instances. You'd have to rewrite it like this:

class Program {
    static void Main(string[] args) {

        TaskScheduler.UnobservedTaskException += ( object sender, UnobservedTaskExceptionEventArgs eventArgs ) =>
        {
            eventArgs.SetObserved();
            ( (AggregateException)eventArgs.Exception ).Handle( ex =>
            {
                Console.WriteLine("Exception type: {0}", ex.GetType());
                return true;
            } );
        };

        Run();

        GC.Collect();
        GC.WaitForPendingFinalizers();

        Console.WriteLine("done");
        Console.ReadLine();
    }

    static void Run() {
        Task task1 = new Task(() => {
            throw new ArgumentNullException();
        });

        Task task2 = new Task(() => {
            throw new ArgumentOutOfRangeException();
        });

        task1.Start();
        task2.Start();

        while (!task1.IsCompleted || !task2.IsCompleted) {
            Thread.Sleep(50);
        }
    }
}

Don't do this, use Task.Wait().

Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
1

I run this code on .NET 4.5, Visual Studio 2012 (Debug or Release, doesn't matter), I did not put ThrowUnobservedTaskException in my app.config:

using System;
using System.Threading;
using System.Threading.Tasks;

namespace Test
{
    public static class Program
    {
        private static void Main()
        {
            TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;

            RunTask();

            Thread.Sleep(2000);

            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();

            Console.ReadLine();
        }

        static void TaskScheduler_UnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
        {
            Console.WriteLine("Caught!");
        }

        private static void RunTask()
        {
            Task<int> task = Task.Factory.StartNew(() =>
            {
                Thread.Sleep(1000); // emulate some calculation
                Console.WriteLine("Before exception");
                throw new Exception();
                return 1;
            });
        }
    }
}

And the exception is caught by the UnobservedTaskException handler ("Caught!" is printed).

nightcoder
  • 13,149
  • 16
  • 64
  • 72
  • This is because `ThrowUnobservedTaskException` is only necessary if you want to revert to the 4.0 behavior of a Task exception terminating the process. In 4.5 the default changed to *not* terminate the process. It is not required for the unobserved exception in your example to be caught by the event handler. More info in the remarks [here](http://msdn.microsoft.com/en-us/library/system.threading.tasks.taskscheduler.unobservedtaskexception%28v=vs.110%29.aspx) – BitMask777 Nov 20 '14 at 18:41
  • Works in .NET Core 3.1 – Josh Noe May 28 '20 at 20:09