1

I have a WinForm async GUI app in which I've set up some "global" exception handling in program.cs. I also have a GUI thread that's doing an "await Task.WhenAll()" and catching its exception and throwing the awaited Task.Exception property, so that the AggregateException gets all the way to the exception handler in program.cs (I want to iterate over the inner exceptions and log them).

I can see that the exception being thrown out of my try/catch of the WhenAll() is indeed throwing an AggreateException, but when I debug the handler in program.cs, it's not an AggregateException anymore - it's just the first Exception of the AggregateException. I can't figure out what code is doing this "unwrapping" for me?

Program.cs:

static void Main() {
    Application.ThreadException += new System.Threading.ThreadExceptionEventHandler(Application_ThreadException);
    Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);
    ...
    }

static void Application_ThreadException(object sender, System.Threading.ThreadExceptionEventArgs e) {
            if (e.Exception is AggregateException) {
                // expect to log the contents of (e.Exception as AggregateException).Flatten().InnerExceptions, but exception coming 
                // in is not AggregateException but instead is
                // ApplicationException("message 1")
                }
            else {
                // handling for non aggregate exceptions
                }

In Form1.cs

private async void button1_Click(object sender, EventArgs e) {

            Task overall = Task.WhenAll(
                Task.Run(()=>  { throw new ApplicationException("message 1"); }),
                Task.Run(() => { throw new ApplicationException("message 2"); })
                );

            try {
                await overall;
                }
            catch {
                throw overall.Exception; // this is AggregateException
                }
            }
        }
Michael Ray Lovett
  • 6,668
  • 7
  • 27
  • 36
  • 1
    That global exception handling is there for uncaught exceptions. If you *expect* the exception, don't let it go uncaught. In any case, I expect you can just wrap the `AggregateException` in an exception of your own, if you so desire. – Luaan Apr 06 '16 at 15:36
  • @MichaelRayLovett: Can you post a minimal, reproducible example? – Stephen Cleary Apr 06 '16 at 15:41
  • There is no reason to throw an AggregateException. That exception is *itself* a wrapper over one or more exceptions that occured inside an asynchronous method. `await` *unwraps* it returning the first Inner exception. I suspect there is an `await ... WhenAll();` that throws, resulting in the first exception getting rethrown in the UI thread, *after* the await – Panagiotis Kanavos Apr 06 '16 at 15:42
  • @StephenCleary Minimal, reproducible example now in original post – Michael Ray Lovett Apr 06 '16 at 16:16
  • One thing that's occurred to me that could be related: If I don't have an application level handler at all, and thus my AggreateException goes unhandled, the runtime shows me a message box about the unhandled execption - but it too has "unwrapped" the AggregateException and just shown me InnerExceptions[0]. My point being that this "unwrapping" of AggregateException seems to happen at various points in the runtime, perhaps this is also how Application.ThreadException works? (Though I can't imagine why a user supplied handler wouldn't be handled the Exception type that was actually thrown??) – Michael Ray Lovett Apr 06 '16 at 18:01

2 Answers2

3

It's not just AggregateException - WinForms will always only send GetBaseException() to the handler. That is, only the innermost exception of any InnerException chain.

Apparently this is a longstanding WinForms bug, probably permanent at this point.

You'll have to work around it with your own type:

public class ExceptionWrapper : Exception
{
    public new Exception InnerException { get; set; }
}

throw new ExceptionWrapper { InnerException = overall.Exception };
Community
  • 1
  • 1
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
1

The best possible workaround for this issue is capturing the raised exception via the AppDomain.FirstChangeException event and then comparing this exceptions base exception reference against the exception raised by Application.ThreadException.

Something like this:

private Exception lastFirstChanceException;
AppDomain.CurrentDomain.FirstChanceException += (sender, e) =>
{
   lastFirstChanceException = e.Exception;
};
Application.ThreadException += (sender, e) =>
{
   if (lastFirstChanceException?.GetBaseException() == e.Exception)
   {
       var realException = lastFirstChanceException; // This is the "real" exception thrown by some code
   }
};
David Roth
  • 677
  • 4
  • 14