2

When I write Task-based code, some of my ContinueWith clauses intentionally throw Exceptions (which I catch and handle appropriately), and some of them accidentally throw Exceptions (because of bugs). How can I avoid breaking on the first kind while still breaking on the second kind?

In the code below, I expect the debugger to break on the Unintentional Exception, and NOT on the Intentional Exception (because it is handled later). If I disable "Just My Code" according to this question, then the Intentional Exception doesn't break the debugger (correct), but the Unintentional Exception doesn't break the debugger either (incorrect). If I enable "Just My Code", then the Unintentional Exception does break the debugger (correct), but so does the Intentional Exception (incorrect).

Is there any setting to make Exceptions in ContinueWith clauses work like a normal developer would presumably expect them to?

class Program
{
    static void Main(string[] args)
    {
        //Intentional Exception
        Task.Factory
            .StartNew(() => Console.WriteLine("Basic action"))
            .ContinueWith((t1) => { throw new Exception("Intentional Exception"); })
            .ContinueWith((t2) => Console.WriteLine("Caught '" + t2.Exception.InnerException.Message + "'"));

        //Unintentional Exception
        Task.Factory
            .StartNew(() => Console.WriteLine("Basic action"))
            .ContinueWith((t3) => { throw new Exception("Unintentional Exception (bug)"); });

        Console.ReadLine();
    }
}
Community
  • 1
  • 1
Ben
  • 1,272
  • 13
  • 28

2 Answers2

1

There are two possible approaches to address this problem. Neither are very satisfying as they essentially rely on the developer detecting 100% their own bugs which is really something the debugger should be doing in a development setting. For either approach, the developer should first turn off "Just My Code" as this is the way to prevent the caught/observed Intentional Exception from causing the debugger to break.

First, as @Peter Deniho mentions in his answer and its comments, the developer can listen for the TaskScheduler.UnobservedTaskException event. The code for this is simple:

TaskScheduler.UnobservedTaskException += (sender, e) =>
{
    throw new Exception("An Exception occurred in a Task but was not observed", e.Exception.InnerException);
};

The difficulty with this approach is that this event only fires when the faulted Task's finalizer is called. This happens during garbage collection, but garbage collection may not happen until long after the Exception occurs. To see that this is a serious issue, use the approach mentioned later in this paragraph, but comment out GC.Collect(). In that scenario, the Heartbeats will go on for a very long time before the unobserved Exception is thrown. To deal with this issue, garbage collection must be forced as frequently as you want to make sure your unobserved/Unintentional Exception is reported to the developer. This can be accomplished by defining the following method and calling it at the beginning of the original static void Main:

private static async void WatchForUnobservedExceptions()
{
    TaskScheduler.UnobservedTaskException += (sender, e) =>
    {
        throw new Exception("An Exception occurred in a Task but was not observed", e.Exception.InnerException);
    };

    //This polling loop is necessary to ensure the faulted Task's finalizer is called (which causes UnobservedTaskException to fire) in a timely fashion
    while (true)
    {
        Console.WriteLine("Heartbeat");
        GC.Collect();
        await Task.Delay(5000);
    }
}

Of course, this has serious performance implications and the fact that this is necessary is a pretty good indicator of fundamentally broken code.

The above approach could be characterized as periodically polling to automatically check Tasks' Exceptions that might have been missed. A second approach is to explicitly check each Task's Exception. That means the developer must identify any "background" Tasks whose Exceptions are not ever observed (via await, Wait(), Result, or Exception) and do something with any Exceptions that are unobserved. This is my preferred extension method:

static class Extensions
{
    public static void CrashOnException(this Task task)
    {
        task.ContinueWith((t) =>
        {
            Console.WriteLine(t.Exception.InnerException);
            Debugger.Break();
            Console.WriteLine("The process encountered an unhandled Exception during Task execution.  See above trace for details.");
            Environment.Exit(-1);
        }, TaskContinuationOptions.OnlyOnFaulted);
    }
}

Once defined, this method can simply be appended to any Task that might otherwise have unobserved Exceptions if it had bugs:

Task.Factory
    .StartNew(() => Console.WriteLine("Basic action"))
    .ContinueWith((t3) => { throw new Exception("Unintentional Exception (bug)"); })
    .CrashOnException();

The primary drawback of this approach is that each approriate task must be identified (most Tasks will not need or want this modification since most Tasks are used at some future point in the program) and then suffixed by the developer. If the developer forgets to suffix a Task with an unobserved Exception, it will be silently swallowed just like the behavior of the Unintentional Exception in the original question. This means that this approach is less effective at catching bugs due to carelessness even though doing that is what this question is about. In exchange for the reduced likelihood of catching bugs, you do not take the performance hit of the first approach.

Community
  • 1
  • 1
Ben
  • 1,272
  • 13
  • 28
0

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:

enter image description here

…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.

Peter Duniho
  • 68,759
  • 7
  • 102
  • 136
  • I've removed the confusing WriteLine; thanks. However, it seems like the main difference between your code and mine is the presence of Wait(). If I append .Wait() to each of my final ContinueWith clauses, I do get the desired behavior. But, that also makes them run synchronously which is undesirable; I want them in the background. The root of the question is how I can catch bugs in background (unWaited and un-awaited) ContinueWiths without having them swallowed. Using Just My Code might work, but then I can never run code unattended that allows an Exception to cross a ContinueWith boundary. – Ben Mar 31 '15 at 08:20
  • @Ben: the `Wait()` is just there for the purposes of the demonstration. No code would ever even have the chance to observe an exception unless it attempts to resolve the task, e.g. with a wait or retrieving the result (for non-void tasks). So the wait is necessary just to show if and how exceptions _are_ observed. – Peter Duniho Mar 31 '15 at 16:09
  • @Ben: _"If I append .Wait()...I do get the desired behavior"_ -- I'm confused by that, since I still see the debugger break on each thrown exception even with the waits. _"I can never run code unattended"_ -- I'm also confused by that; I thought this is about debugging only? Why would you run the code unattended, but with the debugger attached? – Peter Duniho Mar 31 '15 at 16:09
  • TPL is supposed to be as close to writing normal code as possible. In normal code, if I have an Exception that isn't handled anywhere, I expect the debugger to break on that exception, and then the process to terminate. I expect the same behavior from an Exception that isn't handled anywhere in TPL. The root of the question is how I can catch bugs in background (unobserved) ContinueWiths. It looks like your statement that I simply can't do what I want is true, and I see that as a serious drawback of TPL (even though I'm sure there were good technical reasons for making that design decision). – Ben Mar 31 '15 at 18:37
  • "*I still see the debugger break on each thrown exception even with the waits*" Disable "Just My Code" and you'll see the desired behavior. Enable it and you'll see the behavior you describe. "*Why would you run the code unattended, but with the debugger attached*" Because it takes a few hours to run (during which time I want to do something else), but I still want to have access to the debugging environment when I come back and find that something has gone wrong (just like if the Unintentional Exception occurred in non-TPL code, or in ThreadPool.QueueUserWorkItem) – Ben Mar 31 '15 at 18:47
  • @Ben _"how I can catch bugs in background (unobserved) ContinueWiths"_ -- ah, well that's a somewhat different question than previously asked. And the answer to that is that you handle the `TaskScheduler.UnobservedTaskException` event (see my code example above for an example of this). – Peter Duniho Apr 01 '15 at 02:50
  • @Ben: (continued...) As long as either the exception is propagated out of the task (i.e. by waiting on the task _where the exception is thrown_, or retrieving the result), or you use a continuation to inspect the `Task.Exception` property where it's stored (as in your code), the exception is "observed". Otherwise not...by handling the event, you can log or otherwise report these "unhandled" (actually _unobserved_) exceptions. Granted, by the time the event handler is called, you're no longer in the context where the exception occurred. But there will still be a stack trace, etc. – Peter Duniho Apr 01 '15 at 02:52
  • "*that's a somewhat different question than previously asked*" I'm not sure how; the original question has an observed Exception that I don't want the debugger to break on and an unobserved Exception that I do want the debugger to break on. "*you handle the TaskScheduler.UnobservedTaskException*" It seems like this would be a good solution, but the problem appears to be that this event only fires when the Task's finalizer is called, which might not happen for a very long time after the Exception occurs; I will update the question to show why this is not an acceptable solution. – Ben Apr 01 '15 at 16:59