This post is the second and last of a small series about Control.Invoke and its use when exceptions are thrown and handled. In the
previous post we have seen that an exception loses its original stack trace and if you handle it in the background thread you cannot see any reference to the location where the exception was originally thrown.
Here is the same stack trace shown in the previous post:
at System.Windows.Forms.Control.MarshaledInvoke(Control caller, Delegate method, Object[] args, Boolean synchronous)
at System.Windows.Forms.Control.Invoke(Delegate method, Object[] args)
at System.Windows.Forms.Control.Invoke(Delegate method)
at TestForm.DoTheWork(Object sender) in C:\TestInvoke\Program.cs:line 36
Why does this happen? The stack trace only shows methods that run in the background thread... Well, this makes sense, because the exception is travelling through two stacks of calls: the first was the main thread, and the implementation of the Invoke() method rethrows the same exception so its original StackTrace property is cleared and only the stack of calls made in the background thread remains.
But the exception type remains the same, so why bother? Is the original stack trace so important? Well, in this case, not particularly... there's only one point in the application where a DivideByZeroException could be thrown. But what if our application and the call to the invoked method were more complex? What if the exception was a NullReferenceException? That stack trace information would be of much value, so let's try to figure out a way to preserve it. If we wrap the DivideByZeroException in another exception, in the method invoked, the stack trace of the InnerException (our DivideByZeroException) should be preserved. Let's give it a try: here is the example of the previous post, modified in the ThrowWithNiceStackTrace() method (the invoked method) so that the original exception is wrapped in a new ApplicationException:
using System;
using System.Windows.Forms;
using System.Threading;
static class Program {
[STAThread]
static void Main() {
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new TestForm());
}
}
public class TestForm : Form {
private Button btn;
public TestForm() {
// Just add a simple button with a Click handler
btn = new Button();
btn.Text = "Click me";
btn.Click += new EventHandler(btn_Click);
this.Controls.Add(btn);
}
void btn_Click(object sender, EventArgs e) {
// We are creating a WorkItem to be run
// on a thread from the thread pool
WaitCallback wc = new WaitCallback(this.DoTheWork);
ThreadPool.QueueUserWorkItem(wc);
}
void DoTheWork(object sender) {
// This code is run in a background thread
try {
// We are using Invoke so the call
// to ThrowWithNiceStackTrace() runs in the main thread
this.Invoke(new MethodInvoker(
this.ThrowWithNiceStackTrace));
}
catch (Exception ex) {
Type ext = ex.GetType();
string stackTrace = ex.StackTrace;
}
}
private void ThrowWithNiceStackTrace() {
// This code is run in the main thread;
// calling Control methods is legal
this.btn.Text = "Clicked";
try {
this.ThrowWithLongStackTrace();
}
catch (Exception ex) {
throw new ApplicationException(
"Exception caught in invoked method", ex);
}
}
private void ThrowWithLongStackTrace() {
this.ThrowDivByZero();
}
private void ThrowDivByZero() {
int i = 1 / string.Empty.Length;
}
}
Now we run the sample and in the breakpoint inside the DoTheWork() method we see... the same result??
Yes, the exception thrown by Invoke() is still the DivideByZeroException. Not what you might expect, right? And obviously our precious stack trace information is lost again.
It turns out that Invoke() always throws the innermost exception. Digging through the System.Windows.Forms.dll assembly we can find that a call to Exception.GetBaseException() is made to retrieve the exception that will be rethrown. So not only the stack trace is lost, but also other exceptions we might have thrown to enclose the original exception and add context to it. How bad... this seems to be a bug in the Framework, and googling around we can find that we're not alone, and that at least there is some sort of confirmation of this being a bug in the Invoke method (see
here,
here and
here).
What should we do, if we were interested in that information? Should we implement our Invoke strategy?
Hopefully not... the GetBaseException() method is defined virtual: let's try to fool this behavior, overriding the GetBaseException() method:
using System;
using System.Windows.Forms;
using System.Threading;
static class Program {
[STAThread]
static void Main() {
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new TestForm());
}
}
public class TestForm : Form {
private Button btn;
public TestForm() {
// Just add a simple button with a Click handler
btn = new Button();
btn.Text = "Click me";
btn.Click += new EventHandler(btn_Click);
this.Controls.Add(btn);
}
void btn_Click(object sender, EventArgs e) {
// We are creating a WorkItem to be run
// on a thread from the thread pool
WaitCallback wc = new WaitCallback(this.DoTheWork);
ThreadPool.QueueUserWorkItem(wc);
}
void DoTheWork(object sender) {
// This code is run in a background thread
try {
// We are using Invoke so the call
// to ThrowWithNiceStackTrace() runs in the main thread
this.Invoke(new MethodInvoker(
this.ThrowWithNiceStackTrace));
}
catch (Exception ex) {
Type ext = ex.GetType();
string stackTrace = ex.StackTrace;
}
}
private void ThrowWithNiceStackTrace() {
// This code is run in the main thread;
// calling Control methods is legal
this.btn.Text = "Clicked";
try {
this.ThrowWithLongStackTrace();
}
catch (Exception ex) {
throw new InvokeException(
"Exception caught in invoked method", ex);
}
}
private void ThrowWithLongStackTrace() {
this.ThrowDivByZero();
}
private void ThrowDivByZero() {
int i = 1 / string.Empty.Length;
}
}
public class InvokeException : Exception {
public InvokeException(string message, Exception innerException)
: base(message, innerException) { }
public override Exception GetBaseException() {
// Yes, we are lying: the doc says we should return
// 'The first exception thrown in a chain of exceptions'
return this;
}
}
When running this version of the program, if we stop at our breakpoint inside the catch clause of DoTheWork() we find that the exception caught is of type InvokeException, with this stack trace:
at System.Windows.Forms.Control.MarshaledInvoke(Control caller, Delegate method, Object[] args, Boolean synchronous)
at System.Windows.Forms.Control.Invoke(Delegate method, Object[] args)
at System.Windows.Forms.Control.Invoke(Delegate method)
at TestForm.DoTheWork(Object sender) in C:\TestInvoke\Program.cs:line 36
But now there is an InnerException of type DivideByZeroException, with the expected stack trace:
at TestForm.ThrowDivByZero() in C:\TestInvoke\Program.cs:line 60
at TestForm.ThrowWithLongStackTrace() in C:\TestInvoke\Program.cs:line 57
at TestForm.ThrowWithNiceStackTrace() in C:\TestInvoke\Program.cs:line 49
Not an elegant solution: we have to add a try catch in our methods called via Invoke(), at least in the methods where the location and nature of possible exceptions is not obvious. And we have to break the 'contract' of the Exception object: the implementation of GetBaseException of our InvokeException class will deliberately lie to callers. I know that lying is a bad thing (see
Marcelo's recent post about this), but if we limit its usage to this workaround, we should be safe. It will also be easy to remove the workaround one day, when the fix to Windows.Forms is available: that day, just remove the InvokeException class and where compilation errors appear, there are the pieces of code to remove.
Less than nothing, anyway. I have used this workaround in error logs that we have in production, and I find it useful.