4

I cannot catch an unhandled exception that is thrown from a continuation task.

To demonstrate this problem, let me show you some code that works. This code is from a basic Windows Forms application.

First, program.cs:

using System;
using System.Windows.Forms;

namespace WindowsFormsApplication3
{
    static class Program
    {
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);

            Application.ThreadException += (sender, args) =>
            {
                MessageBox.Show(args.Exception.Message, "ThreadException");
            };

            AppDomain.CurrentDomain.UnhandledException += (sender, args) =>
            {
                MessageBox.Show(args.ExceptionObject.ToString(), "UnhandledException");
            };

            try
            {
                Application.Run(new Form1());
            }

            catch (Exception exception)
            {
                MessageBox.Show(exception.Message, "Application.Run() exception");
            }
        }
    }
}

This subscribes to all the exception handlers that are available. (Only Application.ThreadException is actually raised, but I wanted to ensure that I eliminated all other possibilities.)

Now here's the form that results in a message correctly being shown for an unhandled exception:

using System;
using System.Threading;
using System.Windows.Forms;

namespace WindowsFormsApplication3
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        protected override void OnShown(EventArgs e)
        {
            base.OnShown(e);
            doWork();
            this.Close();
        }

        void doWork()
        {
            Thread.Sleep(1000); // Simulate work.
            throw new InvalidOperationException("TEST");
        }
    }
}

When you run this, after one second a message box will appear showing the exception message.

As you can see, I'm writing a "please wait" style form that does some background work and then automatically closes when the work is done.

Accordingly, I added a background task to OnShown() as follows:

protected override void OnShown(EventArgs e)
{
    base.OnShown(e);

    Task.Factory.StartNew(doWork).ContinueWith
    (
        antecedent =>
        {
            if (antecedent.Exception != null)
                throw antecedent.Exception;

            this.Close();
        },
        TaskScheduler.FromCurrentSynchronizationContext()
    );
}

I thought that the continuation would be run in the form's context, and the exception would somehow be caught by one of the unhandled exception events I subscribed to in program.cs.

Unfortunately nothing is caught and the form stays open with no indication that anything went wrong.

Does anyone know how I should do this, such that if an exception is thrown in the worker task and not explicitly caught and handled, it will be caught by an outer unhandled exception event?


[EDIT]

Niyoko Yuliawan suggested using await. Unfortunately I can't use that, because this project is stuck using the ancient .Net 4.0. However, I can confirm that using await WOULD solve it if I could use it!

For completeness, here's the - much simpler and more readable - solution that I could use if I was using .Net 4.5 or later:

protected override async void OnShown(EventArgs e)
{
    base.OnShown(e);
    await Task.Factory.StartNew(doWork);
    this.Close();
}

[EDIT2]

raidensan also suggested a useful-looking answer, but unfortunately that doesn't work either. I think it just moves the problem around slightly. The following code also fails to cause an exception message to be shown - even though running it under the debugger and setting a breakpoint on the line antecedent => { throw antecedent.Exception; } shows that line is being reached.

protected override void OnShown(EventArgs e)
{
    base.OnShown(e);

    var task = Task.Factory.StartNew(doWork);

    task.ContinueWith
    (
        antecedent => { this.Close(); },
        CancellationToken.None,
        TaskContinuationOptions.OnlyOnRanToCompletion, 
        TaskScheduler.FromCurrentSynchronizationContext()
    );

    task.ContinueWith
    (
        antecedent => { throw antecedent.Exception; },
        CancellationToken.None,
        TaskContinuationOptions.OnlyOnFaulted,
        TaskScheduler.FromCurrentSynchronizationContext()
    );
}
Matthew Watson
  • 104,400
  • 10
  • 158
  • 276
  • I'm not aware if the exception which is thrown is non-CLS complaint, but if it is you should catch it only with catch { }. https://msdn.microsoft.com/en-us/bb264489.aspx – mybirthname Oct 28 '16 at 09:50
  • @mybirthname No, it's a basic `InvalidOperationException` which is CLS compliant. Note that the exception is caught correctly when not thrown from a background task. – Matthew Watson Oct 28 '16 at 09:52
  • @NiyokoYuliawan That might work, but unfortunately (as per the `.net-4.0` tag on the question) I cannot use it. – Matthew Watson Oct 28 '16 at 10:03
  • Yes, sorry, I don't see your tag. – Niyoko Oct 28 '16 at 10:03
  • @NiyokoYuliawan It was a good idea though, and in fact it does work if you can use it (pity I can't!) - I'll add something about that to the question! – Matthew Watson Oct 28 '16 at 10:05
  • https://blogs.msdn.microsoft.com/pfxteam/2011/09/28/task-exception-handling-in-net-4-5/ I found this article in which is explained in first how .Net 4.0 exception are handled. PRobably I'm putting you in wrong direction, I don't have great experience with Task. You can check it if you want. They are talking about Unobserved Exceptions. – mybirthname Oct 28 '16 at 10:08
  • I think I found the answer, check [here](http://stackoverflow.com/a/12981091/2928544) – raidensan Oct 28 '16 at 10:09
  • @MatthewWatson I think you should execute Task.WaitAll(yourTask) to catch the exception. https://msdn.microsoft.com/en-us/library/dd537614(v=vs.110).aspx. See second example – mybirthname Oct 28 '16 at 10:27
  • @mybirthname But where would I put such a wait? I can't block the UI so I can't wait in `OnShown() - so I used a continuation to wait for the `doWork()` to complete (which is equivalent to waiting for the task to complete). – Matthew Watson Oct 28 '16 at 10:32
  • @MatthewWatson check my updated answer. – raidensan Oct 28 '16 at 11:22

4 Answers4

2

Have u checked TaskScheduler.UnobservedTaskException event here. With this you can catch unobserved exception happens in worker thread after garbage collection happens.

    static void Main()
    {
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);

        TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;

        Application.ThreadException += (sender, args) =>
        {
            MessageBox.Show(args.Exception.Message, "ThreadException");
        };

        AppDomain.CurrentDomain.UnhandledException += (sender, args) =>
        {
            MessageBox.Show(args.ExceptionObject.ToString(), "UnhandledException");
        };

        try
        {
            Application.Run(new Form1());
        }

        catch (Exception exception)
        {
            MessageBox.Show(exception.Message, "Application.Run() exception");
        }
    }
    private static void TaskScheduler_UnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e)
    {
        throw e.Exception;
    }

Here's task:

protected override void OnShown(EventArgs e)
{
    base.OnShown(e);
    Task.Factory.StartNew(doWork);
    Thread.Sleep(2000);
    GC.Collect();
    GC.WaitForPendingFinalizers();
}

void doWork()
{
    Thread.Sleep(1000); // Simulate work.
    throw new InvalidOperationException("TEST");
}
MKMohanty
  • 956
  • 7
  • 23
  • Unfortunately that doesn't work - I think the exception is being "observed" when I do `if (antecedent.Exception != null)` so the event won't be thown (although it doesn't seem to fire even if I comment that line out...) – Matthew Watson Oct 28 '16 at 10:15
  • ya u r right.. didnt check with continuewith .. will update my answer after i find something – MKMohanty Oct 28 '16 at 10:23
2

Even if you throw your exception on UI thread, it does not mean it will eventually get to Application.ThreadException. In this case, it is intercepted by Task exception handler (handler which sets Task.Exception, Task.IsFaulted and so on), and actually becomes unobserved task exception. Think of it as Task uses Control.Invoke to execute your continuation, and Control.Invoke is synchronous, which means exception from it cannot be delivered to Application.ThreadException, because winforms does not know if it will be handled by caller or not (that's unlike Control.BeginInvoke, exceptions from which will always be delivered to Application.ThreadException).

You can verify that by subscribing to TaskScheduler.UnobservedTaskException and force garbage collection some time later after your continuation is completed.

So long story short - whether your continuation runs on UI thread on not is irrelevant in this case - all unhandled exceptions from your continuation will end as UnobservedTaskException, not ThreadException. It's reasonable to manually call your exception handler in this case and not rely on those "last resort" handlers.

Evk
  • 98,527
  • 8
  • 141
  • 191
  • I see what you're saying, but I can't manually call the exception handler because the form is in a class library and the exception handler is in an application that creates the form, so I need a solution which would result in being able to catch the exception in a "last chance" outer exception handler. It seems that the solution I've come up with is likely the only one that will work (without being able to use `async`). – Matthew Watson Oct 28 '16 at 11:06
  • This answer *does* explain why using a continuation doesn't work though! – Matthew Watson Oct 28 '16 at 11:09
  • Well that's dirty hack and bad design to do it like that, but yes you can use BeginInvoke if nothing else helps. At least now you know why it does not work in continuation. – Evk Oct 28 '16 at 11:09
  • So how would you solve the problem, given that the form doesn't have access to a specific exception handler and you need to be able to report any unhandled exceptions from the application? It's normal to throw exceptions if something bad happens... (and in fact if you can use `await`, that's exactly how it works - it rethrows in the context of the calling code). – Matthew Watson Oct 28 '16 at 11:10
  • I'd provide form with that exception handler :) Whatever creates that form does have access to exception handler and so can pass it to form constructor (or set some form property) in one form or another (some ILogger interface). It can also be done with some dependency injection container if you use one. If all that does not help - at least you can provide exception handler as global object and make it available to all forms (placing it in separate dll). – Evk Oct 28 '16 at 11:13
  • Hmm well I really can't see the point of that. Any code could throw an exception, so you always need an outer exception handler if you want to report errors properly - I don't think there's anything special about that particular form which would warrant special exception handling for it. Your approach would end up with an exception handler being passed to every form in the entire system, which sounds like a bad idea to me. Remember, the difficulty here is an artefact of an internal implementation of the form, and exposing that to all and sundry isn't a good plan, I think. – Matthew Watson Oct 28 '16 at 11:19
  • But with current approach you kind of abusing that unhandled exception handler. First, in this case exception is not unexpected-you were expecting it and you know why and where it happened. This valuable information is lost when you just rethrow exception. All you have in that "catch all" handler is callstack (part of which you also lost when rethrowing). If you called exception handler explicitly, you can provide much more details which would help you later when debugging the problem. You may not necessary pass handler to every form - you can as well use some global (static) class to do that – Evk Oct 28 '16 at 11:40
  • I actually don't know what exception is going to be thrown. In the original code (rather than my simplified example), it was caused by a directory being missing. When rethrowing in my actual code, I retain the original exception as an inner exception, so no information is lost. I think we're just going to have to agree to disagree on this one (part of the problem is of course you are only seeing my simplified example rather than the actual architecture and context). – Matthew Watson Oct 28 '16 at 12:34
1

Update

You have mentioned that you are using .Net 4.0. How about employ async/await feature, which wont block UI. You just need to add Microsoft Async to your project from nuget.

enter image description here

Now modify OnShown as below (I added code for doWork so it can be more obvious):

        protected async override void OnShown(EventArgs e)
        {
            base.OnShown(e);

            await Task.Factory.StartNew(() => 
            {
                Thread.Sleep(1000); // Simulate work.
                throw new InvalidOperationException("TEST");
            })
                .ContinueWith
                (
                    antecedent => { this.Close(); },
                    CancellationToken.None,
                    TaskContinuationOptions.OnlyOnRanToCompletion,
                    TaskScheduler.FromCurrentSynchronizationContext()
                )
                .ContinueWith
                (
                    antecedent => { 
                        throw antecedent.Exception;
                    },
                    CancellationToken.None,
                    TaskContinuationOptions.OnlyOnFaulted,
                    TaskScheduler.FromCurrentSynchronizationContext()
                );

        }

Old answer Found a solution, add task.Wait();. Not sure why it works:

        protected override void OnShown(EventArgs e)
        {
            base.OnShown(e);

            var task = Task.Factory.StartNew(doWork)
                .ContinueWith
                (
                    antecedent => { this.Close(); },
                    CancellationToken.None,
                    TaskContinuationOptions.OnlyOnRanToCompletion,
                    TaskScheduler.FromCurrentSynchronizationContext()
                )
                .ContinueWith
                (
                    antecedent => { 
                        throw antecedent.Exception;
                    },
                    CancellationToken.None,
                    TaskContinuationOptions.OnlyOnFaulted,
                    TaskScheduler.FromCurrentSynchronizationContext()
                );

            // this is where magic happens.
            task.Wait();
        }
raidensan
  • 1,099
  • 13
  • 31
  • 2
    That works because it turns it into a synchronous call - alas, we can't use that because it blocks the UI in `OnShown()` - and not blocking the UI is the whole point of using a background task! – Matthew Watson Oct 28 '16 at 10:44
  • You are right. This is not an answer but I'm gonna keep it for personal history. – raidensan Oct 28 '16 at 10:52
1

I found a workaround which I will post as an answer (but I won't mark this as the answer for at least a day in case someone comes up with a better answer in the meantime!)

I decided to go old-school and use BeginInvoke() instead of a continuation. Then it seems to work as expected:

protected override void OnShown(EventArgs e)
{
    base.OnShown(e);

    Task.Factory.StartNew(() =>
    {
        try
        {
            doWork();
        }

        catch (Exception exception)
        {
            this.BeginInvoke(new Action(() => { throw new InvalidOperationException("Exception in doWork()", exception); }));
        }

        finally
        {
            this.BeginInvoke(new Action(this.Close));
        }
    });
}

However, at least I now know why my original code wasn't working. See Evk's answer for the the reason!

Matthew Watson
  • 104,400
  • 10
  • 158
  • 276