[聚合问答] TAP global exception handler

c#,.net,exception,task-parallel-library,async-await 2017-11-30 10 阅读

This code throws an exception. Is it possible to define an application global handler that will catch it?

string x = await DoSomethingAsync();

Using .net 4.5 / WPF

4个回答

18

This is actually a good question, if I understood it correctly. I initially voted to close it, but now retracted my vote.

It is important to understand how an exception thrown inside an async Task method gets propagated outside it. The most important thing is that such exception needs to be observed by the code which handles the completion of the task.

For example, here is a simple WPF app, I'm on NET 4.5.1:

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

namespace WpfApplication_22369179
{
    public partial class MainWindow : Window
    {
        Task _task;

        public MainWindow()
        {
            InitializeComponent();

            AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
            TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;

            _task = DoAsync();
        }

        async Task DoAsync()
        {
            await Task.Delay(1000);

            MessageBox.Show("Before throwing...");

            GCAsync(); // fire-and-forget the GC

            throw new ApplicationException("Surprise");
        }

        async void GCAsync()
        {
            await Task.Delay(1000);

            MessageBox.Show("Before GC...");

            // garbage-collect the task without observing its exception 
            _task = null;
            GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
        }

        void TaskScheduler_UnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
        {
            MessageBox.Show("TaskScheduler_UnobservedTaskException:" + e.Exception.Message);
        }

        void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
        {
            MessageBox.Show("CurrentDomain_UnhandledException:" + ((Exception)e.ExceptionObject).Message);
        }
    }
}

Once ApplicationException has been thrown, it goes unobserved. Neither TaskScheduler_UnobservedTaskException nor CurrentDomain_UnhandledException gets invoked. The exception remains dormant until the _task object gets waited or awaited. In the above example it never gets observed, so TaskScheduler_UnobservedTaskException will be invoked only when the task gets garbage-collected. Then this exception will be swallowed.

The old .NET 4.0 behavior, where the AppDomain.CurrentDomain.UnhandledException event gets fired and the app crashes, can be enabled by configuring ThrowUnobservedTaskExceptions in app.config:

<configuration>
    <runtime>
      <ThrowUnobservedTaskExceptions enabled="true"/>
    </runtime>
</configuration>

When enabled this way, AppDomain.CurrentDomain.UnhandledException will still be fired after TaskScheduler.UnobservedTaskException when the exception gets garbage-collected, rather than on the spot where it thrown.

This behavior is described by Stephen Toub in his "Task Exception Handling in .NET 4.5" blog post. The part about task garbage-collection is described in the comments to the post.

That's the case with async Task methods. The story is quite different for async void methods, which are typically used for event handlers. Let's change the code this way:

public MainWindow()
{
    InitializeComponent();

    AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
    TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;

    this.Loaded += MainWindow_Loaded;
}

async void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
    await Task.Delay(1000);

    MessageBox.Show("Before throwing...");

    throw new ApplicationException("Surprise");
}

Because it's async void there's no Task reference to hold on to (so there's nothing to be possibly observed or garbage-collected later). In this case, the exception is thrown immediately on the current synchronization context. For a WPF app, Dispatcher.UnhandledException will be fired first, then Application.Current.DispatcherUnhandledException, then AppDomain.CurrentDomain.UnhandledException. Finally, if none of these events are handled (EventArgs.Handled is not set to true), the app will crash, regardless of the ThrowUnobservedTaskExceptions setting. TaskScheduler.UnobservedTaskException is not getting fired in this case, for the same reason: there is no Task.

2017-11-30
1

EDITED as per @Noseration's comment

In .NET 4.5 in async code you can handle unobserved exceptions by registering a handler for the TaskScheduler.UnobservedTaskException event. An exception is deemed unobserved if you do not access the Task.Result, Task.Exception properties and you do not call Task.Wait.

After the unobserved exception reaches the TaskScheduler.UnobservedTaskException event handler, the default behaviour is to swallow this exception so the program does not crash. This behaviour can be changed in the configuration file by adding the following:

<configuration> 
   <runtime> 
      <ThrowUnobservedTaskExceptions enabled="true"/> 
   </runtime> 
</configuration>

2017-11-30
1

Binding an event to the AppDomain.CurrentDomain.FirstChanceException will guarantee you that your exception will be caught. As @Noseratio pointed out, you'll be notified of every exception in your application, even if the exception is handled gracefully within a catch block and the application continues on.

However, I still see this event being useful for at least capturing the last few exceptions thrown before an application halted or perhaps some other debugging scenario.

If you want to protect yourself against this

string x = await DoSomethingAsync();

My advice to you is, don't do that, add a try catch block :-)

2017-11-30
1

You can always do the following to handle the exception using Application.DispatcherUnhandledException method. Of course it would be given to you inside a TargetInvocationException and might not be as pretty as other methods. But it works perfectly fine

_executeTask = executeMethod(parameter);
_executeTask.ContinueWith(x =>
{
    Dispatcher.CurrentDispatcher.Invoke(new Action<Task>((task) =>
    {
        if (task.Exception != null)
           throw task.Exception.Flatten().InnerException;
    }), x);
}, TaskContinuationOptions.OnlyOnFaulted);

2017-11-30

注:本文内容来自互联网,旨在为开发者提供分享、交流的平台。如有涉及文章版本等事宜,请你联系站长进行处理。