Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

system.threading.task - why does TaskScheduler.UnobservedTaskException event not occur? Can I fix this?

A common problem I've seen has been managing unhandled exceptions inside of tasks. They don't cause a crash, they happen silently, and I can't even get an event to trigger when the task fails! I've seen users prescribe custom classes and stuff to handle this, my question is, is there a "standard" microsoft way to handle this?

For the sake of example, I made this simple console application (.NET 4.5.1) to demonstrate the problem. Can this be modified so that these tasks can be executed asynchronousyly, but call "handler" when it encounters an unhandled exception? Or at least crash? I thought this was what UnobservedTaskException is exactly supposed to do.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ConsoleApplication4
{
class Program
{
    static void Main(string[] args)
    {
        TaskScheduler.UnobservedTaskException += Handler;
        AppDomain.CurrentDomain.UnhandledException += Handler;
        Task.Run(() => { throw new ApplicationException("I'll throw an unhandled exception"); });
        Task.Factory.StartNew(() => { throw new ApplicationException("I'll throw an unhandled exception too"); });
        System.Threading.Thread.Sleep(2000);
        Console.WriteLine("I think everything is just peachy!");
        System.Threading.Thread.Sleep(10000);
    }
    private static void Handler(Object sender, EventArgs e)
    {
        Console.WriteLine("I'm so lonely, won't anyone call me?");
    }
}
}

Output:

I think everything is just peachy!

Desired output:

I'm so lonely, won't anyone call me?
I'm so lonely, won't anyone call me?
I think everything is just peachy!

And/or simply crashing, even that would be a tremendous improvement over the asynchronous task failing silently!

EDIT: added this to app.config per this MSDN article http://msdn.microsoft.com/en-us/library/jj160346%28v=vs.110%29.aspx, but no change:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <runtime>
    <ThrowUnobservedTaskExceptions enabled="true"/>
  </runtime>
</configuration>
like image 409
John Avatar asked May 06 '14 21:05

John


2 Answers

From the comment:

I can see that there is a reason for everything, but I can't deny being flabbergasted by the idea that there's a good reason that this should not happen by design. What if I don't want to await a Task, should I just never use the Task class in that case? What's the drawback to this having an event that can be subscribed to? I'm going to write an extension method, and by all means, tell me why it shouldn't be this way

The nature of the Task object is that it is something that gets completed in the future. The result or exception of the task is also expected to get observed in the future, quite likely on a different stack frame or even on a different thread. That's why the Framework (or compiler-generated code, for async Task methods) stores the exception inside the task and doesn't throw it immediately.

You do not observe the result of the task here and you do not even store a reference to the task object anywhere. Essentially, you're doing a fire-and-forget call. The compiler warns about that (telling that the task is not awaited), but it doesn't eliminate the fact the the managed Task object still gets created and is hanging around until it gets garbage-collected. At that point, TaskScheduler.UnobservedTaskException event will be fired.

... there's a good reason that should not happen by design

Now if the above approach is a bad design, what would be a good one? If the exception would be thrown immediately, how could the following possibly work:

var task = Task.Run(() => {
    throw new ApplicationException("I'll throw an unhanded exception"); });
Thread.Sleep(1000);
if (task.IsFaulted)
  Console.WriteLine(task.Exception.InnerException.Message);

It simply could not. The code after Sleep would not have a chance to handle the exception its way. Thus, the current behavior is well thought out and makes perfect sense.

If you still want to observe exception as soon as it happens for a fire-and-forget task, use a helper async void method:

public static class TaskExt
{
    public static async void Observe(
        this Task @this,
        bool continueOnCapturedContext = true)
    {
        await @this.ConfigureAwait(continueOnCapturedContext);
    } 
}

static void Main(string[] args)
{
    TaskScheduler.UnobservedTaskException += Handler;
    AppDomain.CurrentDomain.UnhandledException += Handler;

    Task.Run(() => { throw new ApplicationException("I'll throw an unhanded exception"); })
        .Observe();

    Task.Factory.StartNew(() => { throw new ApplicationException("I'll throw an unhanded exception too"); })
        .Observe();

    System.Threading.Thread.Sleep(2000);
    Console.WriteLine("I think everything is just peachy!");
    System.Threading.Thread.Sleep(10000);
}

You can also use an async void lambda for a fire-and-forget call:

Action fireAndForget = async () => 
{ 
    try 
    {
        await Task.Run(...); 
    }
    catch(e) { /* handle errors */ };
};
fireAndForget();

I described the exception propagation behavior for async void methods in more details here.

OK, I get what you're saying. By design, are tasks meant to be never used with a fire and forget pattern? Just dispatch the work to an anonymous thread some other way?

I'm not saying that tasks aren't meant to be used with fire-and-forget pattern. I actually think they're perfectly suited for that. There are some options out there to implement this pattern with tasks, I showed one above, another one is Task.ContinueWith, or you can check this question for some more ideas.

A different matter is that I hardly can imagine a useful scenario where I would not want to observe the completion status of the task, even for a fire-and-forget call like above. What job would such a task do? Even for logging, I'd still want to make sure a log enty was successfully written. Besides, what if the parent process ends before the task is complete? In my experience, I've always used something like QueueAsync to keep tracks of the fired tasks.

like image 189
noseratio Avatar answered Nov 08 '22 07:11

noseratio


You can set ThrowUnobservedTaskExceptions within app.config to turn on this behavior (which was the behavior in .NET 4).

Note that the exception will only get raised once the Task in question gets garbage collected, so it won't occur immediately. This means your test may still not necessarily print out the messages (unless you explicitly called GC.Collect for testing purposes here).

like image 23
Reed Copsey Avatar answered Nov 08 '22 08:11

Reed Copsey