简体   繁体   中英

Prevent outer exception from being discarded when thrown from BeginInvoke

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.)

One way to do it is to put the inner-exception reference in a custom property or the Data dictionary -- ie, 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;
    // ...
}

This is admittedly a hack, but it's the best solution I was able to come up with which supports both global exception handling in WinForms and all exceptions, even with inner exceptions.

Special thanks to yas4891 for the answer which inspired this workaround solution.

In the Program.cs :

internal static class Program
{
    [STAThread]
    static void Main()
    {
        ApplicationConfiguration.Initialize();

        AppDomain.CurrentDomain.FirstChanceException += CurrentDomain_FirstChanceException;
        Application.ThreadException += Application_ThreadException;
        Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException, true);

        Application.Run(new MyMainForm());
    }


    private static void CurrentDomain_FirstChanceException(object sender, FirstChanceExceptionEventArgs e)
    {
        _outermostExceptionCache.AddException(e.Exception);
    }


    private static void Application_ThreadException(object sender, ThreadExceptionEventArgs e)
    {
        Exception exception = null;
        if (e?.Exception != null)
            exception = _outermostExceptionCache.GetOutermostException(e.Exception);
        // Handle exception
    }


    private static OutermostExceptionCache _outermostExceptionCache = new();
}

And for that you'll need the OutermostExceptionCache class:

public class OutermostExceptionCache
{
    public void AddException(Exception ex)
    {
        if ((ex != null) && (ex is not TargetInvocationException))
        {
            Exception innermostException = GetInnermostException(ex);
            lock (_syncRoot)
            {
                RemoveOldEntries();
                _cache[innermostException] = new CacheEntry(ex);
            }
        }
    }


    public Exception GetOutermostException(Exception ex)
    {
        Exception innermostException = GetInnermostException(ex);
        Exception outermostException = null;
        lock (_syncRoot)
        {
            if (_cache.TryGetValue(innermostException, out CacheEntry entry))
            {
                outermostException = entry.Exception;
                _cache.Remove(innermostException);
            }
            else
            {
                outermostException = ex;
            }
        }
        return outermostException;
    }


    private void RemoveOldEntries()
    {
        DateTime now = DateTime.Now;
        foreach (KeyValuePair<Exception, CacheEntry> pair in _cache)
        {
            TimeSpan timeSinceAdded = now - pair.Value.AddedTime;
            if (timeSinceAdded.TotalMinutes > 3)
                _cache.Remove(pair.Key);
        }
    }


    private Exception GetInnermostException(Exception ex)
    {
        return ex.GetBaseException() ?? ex;
    }


    private readonly object _syncRoot = new();
    private readonly Dictionary<Exception, CacheEntry> _cache = new();


    private class CacheEntry
    {
        public CacheEntry(Exception ex)
        {
            Exception = ex;
            AddedTime = DateTime.Now;
        }


        public Exception Exception { get; }
        public DateTime AddedTime { get; }
    }
}

The way this works is by watching every exception, as it is thrown, before the runtime even bubbles the exception up to the nearest catch block. Each time an exception is thrown, it is added to a cache, indexed by the innermost (ie base) exception. Therefore, when an exception is caught and a new exception is thrown, with the original one as its inner exception, the cache is updated with that outer exception. Then, when Application.ThreadException event handler is provided with the unwrapped, innermost, exception, the handler can look up the outermost one from the cache.

Note: Since even locally-caught exceptions will get added to the cache (and therefore never removed via a call to GetOutermostException ), it timestamps each one and automatically ditches any that are older than 3 minutes. That's an arbitrary timeout which can be adjusted as needed. If you make the timeout too short, it could cause problems with debugging since it can cause the exception handling to revert to handling only the innermost exception if you pause the process too long in the debugger (after the exception is thrown but before it is handled).

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

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM