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?
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:
- Handle exceptions within the async method itself; or
- 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.
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.
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.");
}
User contributions licensed under CC BY-SA 3.0