15

Based on my research, I have learned the following:

  1. TaskScheduler.UnobservedTaskException must wait for the task to be garbage collected before that task's unobserved exception will bubble up to the UnobservedTaskException event.
  2. If you're using Task.Wait(), it'll never get called anyway, because you're blocking on an impending result from the Task, hence the exception will be thrown on Task.Wait() rather than bubble up to the UnobservedException event.
  3. Calling GC.Collect() manually is generally a bad idea unless you know exactly what you're doing, hence it's good in this case for confirming things, but not as a proper solution to the issue.

The Problem

If my application exits before the garbage collector kicks in, I absolutely 100% cannot get my UnobservedTaskException event to fire.

Note the following code:

class Program
{
    static void Main(string[] args)
    {
        TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;

        Task.Factory.StartNew(() =>
        {
            Console.WriteLine("Task started.");
            throw new Exception("Test Exception");
        });

        Thread.Sleep(1000);
        //GC.Collect();
        //GC.WaitForPendingFinalizers();
    }

    static void TaskScheduler_UnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
    {
        File.WriteAllText(@"C:\data\TestException.txt", e.Exception.ToString());
        Console.WriteLine("UNOBSERVED EXCEPTION");
    }
}

No exception file is written and nothing is written to the console. 10-15 minutes and more can go by after the application exits and still I see no evidence that my application's remains were garbage collected. You might ask, well why not just collect on exit? Well, my real world scenario is that my exception trapping runs inside a WCF service hosted inside a Windows service. I cannot trap when the Windows service is shutting down (and hence manually call GC.Collect()) because there is no event for that as far as I can see.

Where am I going wrong? How do I ensure that if something deep inside the WCF service is going to ultimately break my windows service, that I have a chance to log the exception before the service falls over?

Kirill Kobelev
  • 10,252
  • 6
  • 30
  • 51
Nathan Ridley
  • 33,766
  • 35
  • 123
  • 197

2 Answers2

9

To me, TaskScheduler.UnobservedTaskException at first gives a very wrong sense of security. It's really not worth much if it depends on Garbage Collection.

I found the following solution, taken from this msdn article, to be much more reliable. It basically executes the continuation block (where you log the exception) only if there were unhandled exceptions in task1, and does not block UI execution.

You might also want to flatten nested AggregateExceptions and perhaps create a extension method, as Reed Copsey depicted here.

var task1 = Task.Factory.StartNew(() =>
{
    throw new MyCustomException("Task1 faulted.");
})
.ContinueWith((t) =>
{
    Console.WriteLine("I have observed a {0}",
        t.Exception.InnerException.GetType().Name);
},
TaskContinuationOptions.OnlyOnFaulted);
Community
  • 1
  • 1
Mike Fuchs
  • 12,081
  • 6
  • 58
  • 71
2

Nathan,

You're points are all true. Try the following:

namespace ConsoleApplication1
{
    using System;
    using System.Threading;
    using System.Threading.Tasks;

    class Program
    {
        static void Main(string[] args)
        {
            TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;

            Task.Factory.StartNew(() =>
            {
                Console.WriteLine("Task started.");
                throw new Exception("Test Exception");
            });

            Thread.Sleep(1000);
            Console.WriteLine("First Collect");
            GC.Collect();
            GC.WaitForPendingFinalizers();
            Console.WriteLine("Waiting");
            Console.ReadKey();
        }

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

I have noticed that the debugger often "traps" the UnobservedTaskException event, causing it to not fire appropriately. Run this outside of the debugger, and it will print "UNOBSERVED EXCEPTION" every time prior to shutting down.

Kirill Kobelev
  • 10,252
  • 6
  • 30
  • 51
Reed Copsey
  • 554,122
  • 78
  • 1,158
  • 1,373
  • 1
    Hi, thanks for the suggestion, although I am actually already running it outside of the debugger. I notice also you're using GC.Collect(). What happens in your example if you do not manually collect? – Nathan Ridley Oct 19 '10 at 22:44
  • 1
    @Nathan: Normally, there's not enough GC pressure to ever have this happen in this simple example. If the GC doesn't collect, the "task" won't hit the finalizer, and the UnobservedException never fires. It only happens when the finalizer collects an unrooted task with an exception. – Reed Copsey Oct 19 '10 at 22:49
  • @Nathan: Note that UnobservedTaskException won't prevent the app from shutting down in any case - you need to handle your exceptions inside of your task, or always wait on a task, if you want to prevent this from shutting down an app. – Reed Copsey Oct 19 '10 at 22:50
  • So the question remains, when you shift my code inside a WCF service hosted inside a windows service, how do I get the event to fire so that if something unexpected takes down the service, I have a log file to go on? – Nathan Ridley Oct 19 '10 at 22:51
  • I don't need the application to stay running, I only need to make sure the exception is logged so that I have an idea what caused the crash. – Nathan Ridley Oct 19 '10 at 22:51
  • @Nathan: You can use UnobservedTaskException as above. If the task takes down the service, it'll get logged. The only time this won't happen is if the task has an exception, but the service is shut down cleanly prior to the garbage collector call. If you want to force it, you could do a GC.Collect to force it - but I wouldn't recommend it (I'd let that very unlikely scenario go). – Reed Copsey Oct 19 '10 at 22:57
  • Your above code invokes GC.Collect(), and your subsequent comments talk about GC pressure resulting in the garbage collector not collecting. I already have a real world commercial service running and it crashes every few days without any form of logging whatsoever. For the life of me, I cannot figure out why AppDomain.UnhandledException will fire, but TaskScheduler.UnobservedException will not. – Nathan Ridley Oct 19 '10 at 23:01
  • @Nathan: Are you getting any messages in the system event logs on the system? (My code was calling GC.Collect just to force the issue, in order to show you that it does work. You could add that to your service during it's shutdown, but it won't have an effect if it's "crashing", only if it was shut down cleanly.) – Reed Copsey Oct 19 '10 at 23:03
  • The event log shows that the service crashed, but doesn't contain any useful information that would give me a starting point to track down the problem. Would it be useful to put `GC.Collect()` in the `Dispose` method of the windows service class? – Nathan Ridley Oct 19 '10 at 23:06
  • @Nathan: It won't have an effect if the service **crashes**, since Dispose() would never get called in that situation. It would only have an effect if the service is shutdown nicely, but still had a task sitting around with a problem. – Reed Copsey Oct 19 '10 at 23:08
  • In the end, I still have no way to guarantee that an unhandled exception thrown inside my service gets logged. – Nathan Ridley Oct 19 '10 at 23:35
  • @Nathan: If it's happening from the task, AND if that's crashing the service, AND if the logging doesn't take too long, it should happen with the event handler. – Reed Copsey Oct 19 '10 at 23:37
  • I'll have to tweak and play around with things a bit I think. I wish there was an answer to the original question above that works without forcing a call to GC.Collect(). Thanks for your help on this one in any case. – Nathan Ridley Oct 19 '10 at 23:41
  • There may be an exception that cannot be handled, like StackOverflowException or something with "out of memory". You won't have any luck with those, no matter what logging you use. Apart from that, all this sounds like something else is crashing your process which is still unhandled or not handled correctly. – ygoe Feb 18 '14 at 21:19