Application being closed even with exceptions treatment

0

I have an action trigged by a button that should cover every possible cases.

private async void btnStart_Click(object sender, EventArgs e)
{
    try
    {
        btnStart.Enabled = false;
        await Task.Delay(1000);
        btnStart.Visible = false;
        btnStop.Visible = true;
        var maxSessions = numericFieldSessions.Value;//to run the same stuff in parallell
        for (var i = 0; i < maxSessions; i++)
        {
            await Task.Run(() =>
            {
                Parallel.Invoke(async () =>
                {
                    while (true)
                    {
                        try
                        {
                            A();
                            await Task.Run(() => { B(); }); //longer operation
                        }
                        catch (CustomExceptionA ex)
                        {
                            DoLog($"Custom Exception A: {ex.Message}");
                        }
                        catch (CustomExceptionB ex)
                        {
                            DoLog($"Custom Exception B: {ex.Message}");
                        }
                        catch (CustomExceptionC ex)
                        {
                            DoLog($"Custom Exception C: {ex.Message}");
                        }
                        catch (Exception ex)
                        {
                            DoLog($"Generic Exception: {ex.Message}");
                        }
                    }
                });

            });
        }
    }
    catch (Exception ex)
    {
        DoLog($"Full Generic Exception: {ex.Message}");
    }
}

DoLog() only writes the string to a File. After a long time, the program just crash. Without logging anything. I saw in the Windows Event Log that an unhandled exception was thrown inside the method B(). But B() itself should not handle errors... and it isn't!

This is the log:

System.Runtime.InteropServices.ExternalException
   em System.Drawing.Image.FromHbitmap(IntPtr, IntPtr)
   em System.Drawing.Image.FromHbitmap(IntPtr)
   em System.Drawing.Icon.BmpFrame()
   em System.Drawing.Icon.ToBitmap()
   em System.Windows.Forms.ThreadExceptionDialog..ctor(System.Exception)
   em System.Windows.Forms.Application+ThreadContext.OnThreadException(System.Exception)
   em System.Windows.Forms.Control.WndProcException(System.Exception)
   em System.Windows.Forms.Control+ControlNativeWindow.OnThreadException(System.Exception)
   em System.Windows.Forms.NativeWindow.Callback(IntPtr, Int32, IntPtr, IntPtr)

And right after this error event there is another (in the same second):

Faulting application name: MyApp.exe, version: 1.0.0.0, timestamp: 0xb5620f2c
   Faulty module name: KERNELBASE.dll, version: 10.0.18362.476, timestamp: 0x540698cd
   Exception code: 0xe0434352
   Fault offset: 0x001135d2
   Failed process ID: 0xf54
   Failed application start time: 0x01d5da61843fe0f8
   Faulting application path: PATH_TO_MY_APP.exe
   Faulting module path: C:\Windows\System32\KERNELBASE.dll
   Report ID: 120a68ca-a077-47a4-ae62-213e146956a6
   Failed package full name:
   Application ID for the failed package:

How to prevent this? I thought that every exception would be handled.. how to prevent this? Assuming that - anything that happens inside B() should be handled outside it?

c#
winforms

1 Answer

0

From Peter Torr's post on Async and Exceptions in C# he makes the following suggestion when dealing with exception handling in async methods:

Basically, in order to be safe you need to do one of two things:

  1. Handle exceptions within the async method itself; or
  2. Return a Task and ensure that the caller attempts to get the result whilst also handling exceptions (possibly in a parent stack frame)

Failure to do either of these things will result in unwanted behaviour.

Reproducing the Error

Because I don't know the method sugnature of your b method, I started with the basic void b(). Using void b() I was unable to reproduce your error in the following snippet:

private async void button1_Click(object sender, EventArgs e)
{
    try
    {
        await Task.Run(() => { b(); });

        //also tried:
        //await Task.Run(b);
        //await Task.Run(new Action(b));
    }
    catch (Exception E)
    {
        MessageBox.Show($"Exception Handled: \"{E.Message}\"");
    }
}

void b()
{
    DateTime begin = DateTime.Now;
    while (DateTime.Now.Subtract(begin).TotalSeconds < 3) //Wait 3 seconds
    { /*Do Nothing*/ }

    //c() represents whichever method you're calling
    //inside of b that is throwing the exception.
    c();
}

void c()
{
    throw new Exception("Try to handle this exception.");
}

In this case, VS did break when the exception is thrown highlighting the throwing line claiming it was an user-unhandled exception, however, continuing execution did catch the exception and the message box was shown. Running the example without the debugger caused no breaks and the MessageBox was shown as expected.

Later on I tried changing the b method and making it an async void:

async void b()
{
    await Task.Run(() =>
    {
        DateTime begin = DateTime.Now;
        while (DateTime.Now.Subtract(begin).TotalSeconds < 10) //Wait 10 seconds
        { /*Do Nothing*/ }
    });
    c();
}

In this scenario, where b is async, I was able to reproduce your error. Visual Studio's debugging still informs me of the exception as soon as it is thrown by highlighting the throwing line, however, continuing execution now breaks the program, and the try-catch block was unable to catch the exception.

This probably happens because async void defines a "Fire-and-Forget" pattern. Even though you're calling it through Task.Run(), the await before Task.Run() IS NOT getting the result of b() because it is still void. This causes the Exception to be left unused until the GC tries to collect it

In Peter Torr's words:

The basic reason for this is that if you don't attempt to get the result of a Task (either by using await or by getting the Result directly) then it just sits there, holding on to the exception object, waiting to get GCed. During GC, it notices that nobody ever checked the result (and therefore never saw the exception) and so bubbles it up as an unobserved exception. As soon as someone asks for the result, the Task throws the exception instead which must then be caught by someone.

The Solution

What solved the issue for me was changing the signature of void b() to async Task b(), also, after this change, instead of calling b through Task.Run() you can now just call it directly with await b(); (see Solution 1 below).

If you have access to b's implementation, but for some reason can't change its signature (for instance, to maintain backwards compatbility), you'll have to use a try-catch block inside of b, but you can't re-throw any exceptions you catch, or the same error will continue (see Solution 2 below).


Solution 1

Change b's signature:

private async void button1_Click(object sender, EventArgs e)
{
    //Now any exceptions thrown inside b, but not handled by it
    //will properly move up the call stack and reach this level
    //where this try-catch block will be able to handle it.
    try
    {
        await b();
    }
    catch (Exception E)
    {
        MessageBox.Show($"Exception Handled: \"{E.Message}\"");
    }
}

async Task b()
{
    await Task.Run(()=>
    {
        DateTime begin = DateTime.Now;
        while (DateTime.Now.Subtract(begin).TotalSeconds < 3) //Wait 3 seconds
        { /*Do Nothing*/ }
    });
    c();
}

void c()
{
    throw new Exception("Try to handle this exception.");
}

Solution 2

Change b's body:

private async void button1_Click(object sender, EventArgs e)
{
    //With this solution, exceptions are treated inside b's body
    //and it will not rethrow the exception, so encapsulating the call to b()
    //in a try-catch block is redundant and unecessary, since it will never
    //throw an exception to be caught in this level of the call stack.

    await Task.Run(() => { b(); });
}

void b()
{
    DateTime begin = DateTime.Now;
    while (DateTime.Now.Subtract(begin).TotalSeconds < 3) //Wait 3 seconds
    { /*Do Nothing*/ }
    try
    {
        c();
    }
    catch (Exception)
    {
        //Log the error here.
        //DO NOT re-throw the exception.
    }
}

void c()
{
    throw new Exception("Try to handle this exception.");
}
answered on Stack Overflow Feb 5, 2020 by Matheus Rocha • edited Feb 5, 2020 by Matheus Rocha

User contributions licensed under CC BY-SA 3.0