How to catch/observe an unhandled exception thrown from a Task

10,839

Solution 1

The TaskScheduler.UnobservedTaskException event should give you what you want, as you stated above. What makes you think that it is not getting fired?

Exceptions are caught by the task and then re-thrown, but not immediately, in specific situations. Exceptions from tasks are re-thrown in several ways (off the top of my head, there are probably more).

  1. When you try and access the result (Task.Result)
  2. Calling Wait(), Task.WaitOne(), Task.WaitAll() or another related Wait method on the task.
  3. When you try to dispose the Task without explicitly looking at or handling the exception

If you do any of the above, the exception will be rethrown on whatever thread that code is running on, and the event will not be called since you will be observing the exception. If you don't have the code inside of a try {} catch {}, you will fire the AppDomain.CurrentDomain.UnhandledException, which sounds like what might be happening.

The other way the exception is re-thrown would be:

  • When you do none of the above so that the task still views the exception as unobserved and the Task is getting finalized. It is thrown as a last ditch effort to let you know there was an exception that you didn't see.

If this is the case and since the finalizer is non-deterministic, are you waiting for a GC to happen so that those tasks with unobserved exceptions are put in the finalizer queue, and then waiting again for them to be finalized?

EDIT: This article talks a little bit about this. And this article talks about why the event exists, which might give you insight into how it can be used properly.

Solution 2

I used the LimitedTaskScheduler from MSDN to catch all exceptions, included from other threads using the TPL:


public class LimitedConcurrencyLevelTaskScheduler : TaskScheduler
{
    /// Whether the current thread is processing work items.
    [ThreadStatic]
    private static bool currentThreadIsProcessingItems;

    /// The list of tasks to be executed.
    private readonly LinkedList tasks = new LinkedList(); // protected by lock(tasks)

    private readonly ILogger logger;

    /// The maximum concurrency level allowed by this scheduler.
    private readonly int maxDegreeOfParallelism;

    /// Whether the scheduler is currently processing work items.
    private int delegatesQueuedOrRunning; // protected by lock(tasks)

    public LimitedConcurrencyLevelTaskScheduler(ILogger logger) : this(logger, Environment.ProcessorCount)
    {
    }

    public LimitedConcurrencyLevelTaskScheduler(ILogger logger, int maxDegreeOfParallelism)
    {
        this.logger = logger;

        if (maxDegreeOfParallelism Gets the maximum concurrency level supported by this scheduler.
    public override sealed int MaximumConcurrencyLevel
    {
        get { return maxDegreeOfParallelism; }
    }

    /// Queues a task to the scheduler.
    /// The task to be queued.
    protected sealed override void QueueTask(Task task)
    {
        // Add the task to the list of tasks to be processed.  If there aren't enough
        // delegates currently queued or running to process tasks, schedule another.
        lock (tasks)
        {
            tasks.AddLast(task);

            if (delegatesQueuedOrRunning >= maxDegreeOfParallelism)
            {
                return;
            }

            ++delegatesQueuedOrRunning;

            NotifyThreadPoolOfPendingWork();
        }
    }

    /// Attempts to execute the specified task on the current thread.
    /// The task to be executed.
    /// 
    /// Whether the task could be executed on the current thread.
    protected sealed override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
    {
        // If this thread isn't already processing a task, we don't support inlining
        if (!currentThreadIsProcessingItems)
        {
            return false;
        }

        // If the task was previously queued, remove it from the queue
        if (taskWasPreviouslyQueued)
        {
            TryDequeue(task);
        }

        // Try to run the task.
        return TryExecuteTask(task);
    }

    /// Attempts to remove a previously scheduled task from the scheduler.
    /// The task to be removed.
    /// Whether the task could be found and removed.
    protected sealed override bool TryDequeue(Task task)
    {
        lock (tasks)
        {
            return tasks.Remove(task);
        }
    }

    /// Gets an enumerable of the tasks currently scheduled on this scheduler.
    /// An enumerable of the tasks currently scheduled.
    protected sealed override IEnumerable GetScheduledTasks()
    {
        var lockTaken = false;

        try
        {
            Monitor.TryEnter(tasks, ref lockTaken);

            if (lockTaken)
            {
                return tasks.ToArray();
            }
            else
            {
                throw new NotSupportedException();
            }
        }
        finally
        {
            if (lockTaken)
            {
                Monitor.Exit(tasks);
            }
        }
    }

    protected virtual void OnTaskFault(AggregateException exception)
    {
        logger.Error(exception);
    }

    /// 
    /// Informs the ThreadPool that there's work to be executed for this scheduler.
    /// 
    private void NotifyThreadPoolOfPendingWork()
    {
        ThreadPool.UnsafeQueueUserWorkItem(ExcuteTask, null);
    }

    private void ExcuteTask(object state)
    {
        // Note that the current thread is now processing work items.
        // This is necessary to enable inlining of tasks into this thread.
        currentThreadIsProcessingItems = true;

        try
        {
            // Process all available items in the queue.
            while (true)
            {
                Task item;
                lock (tasks)
                {
                    // When there are no more items to be processed,
                    // note that we're done processing, and get out.
                    if (tasks.Count == 0)
                    {
                        --delegatesQueuedOrRunning;
                        break;
                    }

                    // Get the next item from the queue
                    item = tasks.First.Value;
                    tasks.RemoveFirst();
                }

                // Execute the task we pulled out of the queue
                TryExecuteTask(item);

                if (!item.IsFaulted)
                {
                    continue;
                }

                OnTaskFault(item.Exception);
            }
        }
        finally
        {
            // We're done processing items on the current thread
            currentThreadIsProcessingItems = false;
        }
    }
}

And than the "registration" of the TaskScheduler as the default using Reflection:


public static class TaskLogging
{
    private const BindingFlags StaticBinding = BindingFlags.Static | BindingFlags.NonPublic;

    public static void SetScheduler(TaskScheduler taskScheduler)
    {
        var field = typeof(TaskScheduler).GetField("s_defaultTaskScheduler", StaticBinding);
        field.SetValue(null, taskScheduler);

        SetOnTaskFactory(new TaskFactory(taskScheduler));
    }

    private static void SetOnTaskFactory(TaskFactory taskFactory)
    {
        var field = typeof(Task).GetField("s_factory", StaticBinding);
        field.SetValue(null, taskFactory);
    }
}

Share:
10,839

Related videos on Youtube

Blake Niemyjski
Author by

Blake Niemyjski

Blake Niemyjski is a full time open source software architect and private pilot. Since the late 90’s, Blake's curiosity has been focused around developing software that helps the masses. As a hobbyist developer turned student and professional, Blake's passion is to further his knowledge and create technologies for the future.

Updated on June 06, 2022

Comments

  • Blake Niemyjski
    Blake Niemyjski almost 2 years

    I'm trying to log / report all unhandled exceptions in my app (error reporting solution). I've come across a scenario that is always unhandled. I'm wondering how would I catch this error in an unhandled manner. Please note that I've done a ton of research this morning and tried a lot of things.. Yes, I've seen this, this and many more. I'm just looking for a generic solution to log unhandled exceptions.

    I have the following code inside of a console test apps main method:

    Task.Factory.StartNew(TryExecute);
    

    or

    Task.Run((Action)TryExecute);
    

    as well as the following method:

    private static void TryExecute() {
       throw new Exception("I'm never caught");
    }
    

    I'm already tried wiring up to the following in my app, but they are never called.

    AppDomain.CurrentDomain.UnhandledException
    TaskScheduler.UnobservedTaskException
    

    In my Wpf app where I initially found this error I also wired up to these events but it was never called.

    Dispatcher.UnhandledException
    Application.Current.DispatcherUnhandledException
    System.Windows.Forms.Application.ThreadException
    

    The only handler that is called ever is:

    AppDomain.CurrentDomain.FirstChanceException
    

    but this is not a valid solution as I only want to report uncaught exceptions (not every exception as FirstChanceException is called before any catch blocks are ever executed / resolved.

    • Mike Dinescu
      Mike Dinescu over 10 years
      Interesting.. I haven't done much with Tasks but for everything else the AppDomain UnhandledException is always invoked for unhandled exceptions in the application. Perhaps the Task API automatically catches all exceptions thrown in Tasks and re-routes them somehow..
    • Blake Niemyjski
      Blake Niemyjski over 10 years
      We'll I'm seeing it hit the UnhandledException if I wait the task. But I can't guarantee that a developer will do that when using this reporting lib.
  • Blake Niemyjski
    Blake Niemyjski over 10 years
    Thanks this sounds like what is happening. I set a breakpoint in the handler and it's never fired. If I Wait() it I get the exception caught in the UnhandledException handler. I'm assuming that that since I wasn't (was debugging a sample submitted to us), that the GC hadn't run yet and that is why I never caught it.. Thanks for your detailed response. It was a huge help.