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

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


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.


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);    }}