I'm pretty sure you can't do what you want here, not exactly.
Note: your code example is a bit misleading. There's no reason the text This line should never print
should in fact never print. You don't wait on the tasks in any way, so the output happens before the tasks even get far enough to throw exceptions.
Here's a code example that illustrates what you're asking about, but which IMHO is a bit more robust and which is a better starting point to demonstrate the different behaviors involved:
TaskScheduler.UnobservedTaskException += (sender, e) =>
{
Console.WriteLine();
Console.WriteLine("Unobserved exception: " + e.Exception);
Console.WriteLine();
};
//Intentional Exception
try
{
Console.WriteLine("Test 1:");
Task.Factory
.StartNew(() => Console.WriteLine("Basic action"))
.ContinueWith((t1) => { throw new Exception("Intentional Exception"); })
.ContinueWith(t2 => Console.WriteLine("Caught '" +
t2.Exception.InnerException.Message + "'"))
.Wait();
}
catch (Exception e)
{
Console.WriteLine("Exception: " + e);
}
Console.WriteLine();
//Unintentional Exception
try
{
Console.WriteLine("Test 2:");
Task.Factory
.StartNew(() => Console.WriteLine("Basic action"))
.ContinueWith(
t3 => { throw new Exception("Unintentional Exception (bug)"); })
.Wait();
}
catch (Exception e)
{
Console.WriteLine("Exception: " + e);
}
Console.WriteLine();
Console.WriteLine("Done running tasks");
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine("Done with GC.Collect() and finalizers");
Console.ReadLine();
The fundamental issue is that even in your first Task
example, you're not really handling the exception. The Task
class already did that. All you're doing is, by adding the continuation, is adding a new Task
that itself won't throw an exception, because of course it doesn't throw an exception.
If you were instead to wait on the original continuation (i.e. the one that throws the exception), you'd find the exception wasn't in fact handled. That task will still throw an exception:
Task task1 = Task.Factory
.StartNew(() => Console.WriteLine("Basic action"))
.ContinueWith(t1 => { throw new Exception("Intentional Exception"); });
Task task2 = task1
.ContinueWith(t2 => Console.WriteLine("Caught '" +
t2.Exception.InnerException.Message + "'"));
task2.Wait();
task1.Wait(); // exception thrown here
I.e. in the above, task2.Wait()
is fine, but task1.Wait()
still throws the exception.
The Task
class does have the idea of whether an exception has been "observed" or not. So even if you don't wait on the first task, as long as you retrieve the Exception
property in the continuation (as your example) does, the Task
class is happy.
But note that if you change the continuation so that it ignores the Exception
property value and you don't wait on the exception-throwing task, when the finalizers run in my version of your example, you will find that there is an unobserved task exception reported.
Task task1 = Task.Factory
.StartNew(() => Console.WriteLine("Basic action"))
.ContinueWith(t1 => { throw new Exception("Intentional Exception"); });
Task task2 = task1
.ContinueWith(t2 => Console.WriteLine("Caught exception"));
task2.Wait();
task1 = null; // ensure the object is in fact collected
So what does all this actually mean in the context of your question? Well, to me it means that the debugger would have to have special logic built into it to recognize these scenarios and understand that even though the exception isn't being handled, it is being "observed" per the rules of the Task
class.
I don't know how much work that would be, but it sounds like a lot of work to me. Furthermore, it would be an example of the debugger including detailed information about a specific class in .NET, something that I think would generally be desirable to avoid. In any case, I doubt that work has been done. As far as the debugger is concerned, in both cases you've got an exception and you didn't handle it. It has no way to tell the difference between the two.
Now, all that said…
It seems to me that in your real code you probably aren't actually throwing the plain vanilla Exception
type. So you do have the option of using the "Debug/Exceptions…" dialog:

…to enable or disable breaking on specific exceptions. For those that you know, in spite of them actually not being handled, are effectively observed and thus you don't need to break in the debugger when they happen, you can disable breaking on those exceptions in the debugger. The debugger will still break on other exceptions, but will ignore the ones you've disabled.
Of course, if there's a possibility you will accidentally fail to catch/handle/observe one of the exceptions that you otherwise are normally correctly dealing with, then this isn't an option. You'll just have to live with the debugger break and continue.
Not ideal, but frankly should not be a huge problem either; after all, exceptions are by definition for exceptional situations. They shouldn't be used for flow control or inter-thread communication, and so should occur only very rarely. Most tasks should be catching their own exceptions and handling them gracefully, so you should not have to be hitting F5 very often.