1

In Windows Forms when an exception is thrown from a call to Control.Invoke or Control.BeginInvoke and it is unhandled by any catch block, it can be handled by a control- or application-level handler via the Windows.Forms.Application.ThreadException event. Its handler takes an EventArgs with a property Exception, which is the exception thrown. However, under the hood, Windows.Forms.Control (undesirably) strips off all but the most inner exception, per this answer.

Found this blog post on it as well, but it doesn't suggest any workaround.

(This currently is causing me to get a trivial stack trace in my error logs--a stack trace that tells me the innermost details of , but there is no calling code mentioned and no way to figure out the code location of such an error.)

Is there a workaround? Is there any way to get all the outer exceptions in my application event handler (which is logging unexpected exceptions for troubleshooting)?

The only suggestion I have found seems to be to catch the exception inside the invoked code and stuff some of its info into Exception.Data, perhaps in a new exception--but if I knew the outer code that caused the exception, I could just fix the bug rather than logging it. Instead, how could I do this globally without wrapping a try-catch around every candidate block of code?

Patrick Szalapski
  • 8,738
  • 11
  • 67
  • 129
  • See if this helps https://stackoverflow.com/questions/15668334/preserving-exceptions-from-dynamically-invoked-methods – Edney Holder Dec 02 '19 at 21:27
  • Thanks, but I don't think that applies, as I am talking about WinForms message loop invocation, not an ordinary Method.Invoke. It is the WinForms loop that intervenes and discards all my outer exceptions before rethrowing the innermost exception only in the main thread. I'll edit to clarify. – Patrick Szalapski Dec 02 '19 at 22:43
  • Would it be practical to change all your code that calls Control.Invoke to instead call a static utility method that you write? Then you could put the try/catch there. – Joe White Dec 03 '19 at 22:47
  • Sadly, it would still have to call control.invoke, which eats the outer exceptions, so I don't see how that would help. – Patrick Szalapski Dec 03 '19 at 22:50

1 Answers1

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.

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