I spawn a foreground thread and a background thread, throwing an exception in each.
using System;
using System.Threading;
namespace OriginalCallStackIsLostOnRethrow
{
class Program
{
static void Main(string[] args)
{
try
{
A2();
// Uncomment this to see how the unhandled
// exception in the foreground thread causes
// the program to terminate
// An exception in this foreground thread
// *does* terminate the program
// var t = new Thread(() => {
// throw new DivideByZeroException();
// });
// t.Start();
}
catch (Exception ex)
{
// I am not expecting anything from the
// threads to come here, which is fine
Console.WriteLine(ex);
}
finally
{
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
}
}
static void A2() { B2(); }
static void B2() { C2(); }
static void C2() { D2(); }
static void D2()
{
Action action = () =>
{
Console.WriteLine($"D2 called on worker #{Thread.CurrentThread.ManagedThreadId}. Exception will occur while running D2");
throw new DivideByZeroException();
Console.WriteLine("Do we get here? Obviously not!");
};
action.BeginInvoke(ar => Console.WriteLine($"D2 completed on worker thread #{Thread.CurrentThread.ManagedThreadId}"), null);
}
}
}
As expected, the unhandled exception in the foreground thread terminates the process. However, the unhandled exception in the background thread just terminates the thread and does not bring the process to a halt, effectively going unobserved and failing silently.
This program, therefore, produces the following output:
Press any key to exit...
D2 called on worker #6. Exception will occur while running D2
D2 completed on worker thread #6
This challenges my understanding about exception handling in threads. My understanding was that regardless of the nature of the thread, an unhandled exception, from v2.0 of the framework onwards, will bring the process to termination.
Here is a quote from the documentation on this topic:
The foreground or background status of a thread does not affect the outcome of an unhandled exception in the thread. In the .NET Framework version 2.0, an unhandled exception in either foreground or background threads results in termination of the application. See Exceptions in Managed Threads.
Further more, the page titled Exceptions in Managed Threads states as follows:
Starting with the .NET Framework version 2.0, the common language runtime allows most unhandled exceptions in threads to proceed naturally. In most cases this means that the unhandled exception causes the application to terminate.
This is a significant change from the .NET Framework versions 1.0 and 1.1, which provide a backstop for many unhandled exceptions — for example, unhandled exceptions in thread pool threads. See Change from Previous Versions later in this topic.
ANOTHER INTERESTING OBSERVATION
Interestingly, if I cause the exception to be thrown in the completion callback instead of the actual action that is being done, the exception on the background thread in that case does cause a termination of the program. For code, please see below.
using System;
using System.Threading;
namespace OriginalCallStackIsLostOnRethrow
{
class Program
{
static void Main(string[] args)
{
try
{
// A2();
A3();
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
finally
{
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
}
}
static void A2() { B2(); }
static void B2() { C2(); }
static void C2() { D2(); }
static void D2()
{
Action action = () =>
{
try
{
Console.WriteLine($"D2 called on worker #{Thread.CurrentThread.ManagedThreadId}. Exception will occur while running D2");
throw new DivideByZeroException();
// Console.WriteLine("Do we get here? Obviously not!");
}
catch(Exception ex)
{
Console.WriteLine(ex);
}
};
action.BeginInvoke(ar => Console.WriteLine($"D2 completed on worker thread #{Thread.CurrentThread.ManagedThreadId}"), null);
}
static void A3() { B3(); }
static void B3() { C3(); }
static void C3() { D3(); }
static void D3()
{
Action action = () => { Console.WriteLine($"D2 called on worker #{Thread.CurrentThread.ManagedThreadId}."); };
action.BeginInvoke(ar =>
{
Console.WriteLine($"D2 completed on worker thread #{Thread.CurrentThread.ManagedThreadId}. Oh, but wait! Exception!");
// This one on the completion callback does terminate the program
throw new DivideByZeroException();
}, null);
}
}
}
YET ANOTHER INTERESTING OBSERVATION
Further, even more interestingly, if you handle the exception in the action that you want to execute using APM, in the catch
block (set a breakpoint in the catch block in D2()
), the Exception
that appears has no stack trace other than the lambda being invoked. It has absolutely no information even about how it got there.
Whereas this is not true for exceptions that you trap in a catch block in the completion callback, as in the case of D3()
.
I am using the C# 6.0 compiler in Visual Studio Community 2015 Edition and my program targets v4.5.2 of the .NET framework.
As PetSerAl points out in the comments section of the question, to get the exception information, it is mandatory to call EndInvoke
from inside the completion callback as shown below.
using System;
using System.Runtime.Remoting.Messaging;
using System.Threading;
namespace OriginalCallStackIsLostOnRethrow
{
class Program
{
static void Main(string[] args)
{
try
{
A2();
// A3();
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
finally
{
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
}
}
static void A2() { B2(); }
static void B2() { C2(); }
static void C2() { D2(); }
static void D2()
{
Action action = () =>
{
Console.WriteLine($"D2 called on worker #{Thread.CurrentThread.ManagedThreadId}. Exception will occur while running D2");
throw new DivideByZeroException();
};
action.BeginInvoke(ar =>
{
((Action)((ar as AsyncResult).AsyncDelegate)).EndInvoke(ar);
Console.WriteLine($"D2 completed on worker thread #{Thread.CurrentThread.ManagedThreadId}");
}, null);
}
static void A3() { B3(); }
static void B3() { C3(); }
static void C3() { D3(); }
static void D3()
{
Action action = () => { Console.WriteLine($"D2 called on worker #{Thread.CurrentThread.ManagedThreadId}."); };
action.BeginInvoke(ar =>
{
try
{
Console.WriteLine($"D2 completed on worker thread #{Thread.CurrentThread.ManagedThreadId}. Oh, but wait! Exception!");
throw new DivideByZeroException();
}
catch (Exception ex)
{
throw ex;
}
}, null);
}
}
}
This is weird, and it still remains a mystery as to why the stack trace does not show up if you were to place a try / catch
block in the action that is executing asynchronously.
I am referring to the absence of the StackTrace, not the absence of a call stack. :-)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With