13

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

Joe White
  • 94,807
  • 60
  • 220
  • 330
  • 3
    Yes, this is intentional. Control.InvokeMarshaledCallbacks() catches any exception so it can be marshaled back to the caller. Relevant when using Invoke() or EndInvoke(). It uses Exception.GetBaseException(). You can't fix that. You can however catch the exception yourself and call your event handler directly in this specific case. – Hans Passant Dec 19 '11 at 18:48
  • 2
    @HansPassant, I see the code in InvokeMarshaledCallbacks that's calling GetBaseException. Since GetBaseException is virtual, that suggested that I could override it. Unfortunately, my overridden GetBaseException method is never called -- so I'm not sure this is the specific code that's causing the problem. – Joe White Dec 19 '11 at 19:01
  • @Joe, using GetBaseException does work for me. – Oliver Bock Jun 11 '16 at 17:02
  • 3
    @HansPassant, do you have any idea why Control.InvokeMarshaledCallbacks() eats the outer exception? – Oliver Bock Jun 11 '16 at 17:17
  • 1
    @HansPassant What is the point of [calling `Exception.GetBaseException()` here](https://github.com/dotnet/winforms/blob/641e93d848dffb79f68ad2b50f3c2ac0987a0c5e/src/System.Windows.Forms/src/System/Windows/Forms/Control.cs#L6608)? It doesn’t provide any benefit, it only reduces the amount of context/stack one sees in the resulting Unhandled Exception dialog that is shown and prevents any `ThreadException` handler from seeing the actual (outer) exception that was thrown. Since this is intentional, what problem were people trying to solve by doing this? Can’t it be fixed? – binki Dec 26 '19 at 18:45

4 Answers4

1

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;
    // ...
}
Joe White
  • 94,807
  • 60
  • 220
  • 330
0

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 (i.e. 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).

Steven Doggart
  • 43,358
  • 8
  • 68
  • 105
0

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

yas4891
  • 4,774
  • 3
  • 34
  • 55
  • The machine is indeed running Windows 7 x64. The app is a 32-bit process, but a quick skim of the article suggests that that may not matter. Giving it a deeper read now... – Joe White Dec 19 '11 at 18:47
  • 1
    I don't think this is the problem. The exception *is* getting through -- it's not getting blocked entirely like the article talks about. It's just that something inside .NET is only passing me the innermost exception instead of the actual exception. – Joe White Dec 19 '11 at 19:21
  • @JoeWhite Ok. It was a wild guess anyway. Does that last paragraph help you in any way? If not, I'll just delete my answer – yas4891 Dec 19 '11 at 19:40
  • 1
    I didn't know about the FirstChanceException event, so you at least taught me something new. But since it's fired every time an exception is thrown, whether handled or not, I can't really see a good way to make use of it here. – Joe White Dec 19 '11 at 20:17
0

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.

Dan Bryant
  • 27,329
  • 4
  • 56
  • 102
  • 1
    Sure. But I want the exception to end up in my ThreadException handler, and that means the UI thread needs to call Wait on my Task (or otherwise re-throw the exception on the UI thread). My actual code is doing a WaitAll inside that BeginInvoke (my sample code just shows a throw for simplicity). WaitAll throws an AggregateException with multiple InnerExceptions, but because of the drill-into-InnerException behavior, I'm only seeing one of the inner exceptions instead of the full AggregateException. – Joe White Dec 19 '11 at 19:19