-->

Prevent outer exception from being discarded when

2019-03-25 11:08发布

问题:

I have a handler for Application.ThreadException, but I'm finding that exceptions aren't always getting passed to it correctly. Specifically, if I throw an exception-with-inner-exception from a BeginInvoke callback, my ThreadException handler doesn't get the outer exception -- it only gets the inner exception.

Example code:

public Form1()
{
    InitializeComponent();
    Application.ThreadException += (sender, e) =>
        MessageBox.Show(e.Exception.ToString());
}
private void button1_Click(object sender, EventArgs e)
{
    var inner = new Exception("Inner");
    var outer = new Exception("Outer", inner);
    //throw outer;
    BeginInvoke(new Action(() => { throw outer; }));
}

If I uncomment the throw outer; line and click the button, then the messagebox shows the outer exception (along with its inner exception):

System.Exception: Outer ---> System.Exception: Inner
--- End of inner exception stack trace ---
at WindowsFormsApplication1.Form1.button1_Click(Object sender, EventArgs e) in C:\svn\trunk\Code Base\Source.NET\WindowsFormsApplication1\Form1.cs:line 55
at System.Windows.Forms.Control.OnClick(EventArgs e)
at System.Windows.Forms.Button.OnClick(EventArgs e)
at System.Windows.Forms.Button.OnMouseUp(MouseEventArgs mevent)
at System.Windows.Forms.Control.WmMouseUp(Message& m, MouseButtons button, Int32 clicks)
at System.Windows.Forms.Control.WndProc(Message& m)
at System.Windows.Forms.ButtonBase.WndProc(Message& m)
at System.Windows.Forms.Button.WndProc(Message& m)
at System.Windows.Forms.Control.ControlNativeWindow.OnMessage(Message& m)
at System.Windows.Forms.Control.ControlNativeWindow.WndProc(Message& m)
at System.Windows.Forms.NativeWindow.Callback(IntPtr hWnd, Int32 msg, IntPtr wparam, IntPtr lparam)

But if the throw outer; is inside a BeginInvoke call, as in the above code, then the ThreadException handler only gets the inner exception. The outer exception gets stripped away before ThreadException is called, and all I get is:

System.Exception: Inner

(There's no call stack here because inner never got thrown. In a more realistic example, where I caught one exception and wrapped it to re-throw, there would be a call stack.)

The same thing happens if I use SynchronizationContext.Current.Post instead of BeginInvoke: the outer exception is stripped off, and the ThreadException handler only gets the inner exception.

I tried wrapping more layers of exceptions around the outside, in case it was just stripping off the outermost exception, but it didn't help: apparently somewhere there's a loop doing something along the lines of while (e.InnerException != null) e = e.InnerException;.

I'm using BeginInvoke because I've got code that needs to throw an unhandled exception to be immediately handled by ThreadException, but this code is inside a catch block higher up the call stack (specifically, it's inside the action for a Task, and Task will catch the exception and stop it from propagating). I'm trying to use BeginInvoke to delay the throw until the next time messages are processed in the message loop, when I'm no longer inside that catch. I'm not attached to the particular solution of BeginInvoke; I just want to throw an unhandled exception.

How can I cause an exception -- including its inner exception -- to reach ThreadException even when I'm inside somebody else's catch-all?

(I can't call my ThreadException-handler method directly, due to assembly dependencies: the handler is hooked by the EXE's startup code, whereas my current problem is in a lower-layer DLL.)

回答1:

I am assuming that you are seeing this behaviour on an x64 Windows system and this is a - rather unknown - implementation detail of x64 Windows. Read up on it here

The article goes into details on how to solve this problem by applying some hotfix, that was allegedly shipped with Win7 SP1, but I ran into this issue a few weeks back on Win7 SP1.

Additionally you could attach to AppDomain.FirstChanceException event which gives you access to every exception before it is passed to the CLR for handling



回答2:

The recommended way to propagate the Exception to a higher layer (aside from implicitly rethrowing by Waiting on the Task) is to remove the catch-all in the Task body and instead register a Fault continuation on the Task using Task.ContinueWith, specifying TaskContinuationOptions.OnlyOnFaulted. If you're working through an intermediate layer and don't have access to the Task, you can further wrap this in your own UnhandledException events to pass the Exception object upward.



回答3:

One way to do it is to put the inner-exception reference in a custom property or the Data dictionary -- i.e., leave the InnerException property null, and carry the reference some other way.

Of course, this requires establishing some kind of convention that can be shared between the throwing code and the handling code. The best would probably be to define a custom exception class with a custom property, in a project that's referenced by both pieces of code.

Sample code (though it needs more comments to explain why it's doing the crazy things it's doing):

public class ExceptionDecorator : Exception {
    public ExceptionDecorator(Exception exception) : base(exception.Message) {
        Exception = exception;
    }
    public Exception Exception { get; private set; }
}

// To throw an unhandled exception without losing its InnerException:
BeginInvoke(new Action(() => { throw new ExceptionDecorator(outer); }));

// In the ThreadException handler:
private void OnUnhandledException(object sender, ThreadExceptionEventArgs e) {
    var exception = e.Exception;
    if (exception is ExceptionDecorator)
        exception = ((ExceptionDecorator) exception).Exception;
    // ...
}