Reputation: 109822
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()
);
}
Upvotes: 4
Views: 1153
Reputation: 109822
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!
Upvotes: 1
Reputation: 1139
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.
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();
}
Upvotes: 1
Reputation: 101623
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.Invok
e 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.
Upvotes: 2
Reputation: 966
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");
}
Upvotes: 2