1

Update:

The below behavior only occurs when using the Debug configuration (not optimizing the code). When I change the configuration to Release or tick the 'Optimize code' checkbox in the Build properties, it works just fine.


I'm trying to catch exceptions which occur within a task using Task.ContinueWith as explained in this answer, but the exceptions are not getting handled. Here's a screenshot.

You can reproduce using the following code:

var task = Task.Factory.StartNew(() => { throw new Exception("Oops"); });

task.ContinueWith(t => { Console.WriteLine(t.Exception.Message); },
                  TaskContinuationOptions.OnlyOnFaulted);

I've also tried the following:

var task = Task.Factory.StartNew(() => { throw new Exception("Oops"); });

task.ContinueWith(t =>
{
    if (task.IsFaulted) Console.WriteLine(task.Exception.Message);
});

Any idea why the exception isn't handled?

  • Possible duplicate of [Proper way of handling exception in task continuewith](https://stackoverflow.com/questions/21520869/proper-way-of-handling-exception-in-task-continuewith) – GSerg Aug 04 '17 at 22:42
  • @GSerg, I've seen this answer already. It has the same code I'm using. Still I have this issue. – 41686d6564 stands w. Palestine Aug 04 '17 at 22:48
  • @AhmedAbdelhameed, apologies: just noticed `Console.ReadLine` in the screenshot – Kirill Shlenskiy Aug 05 '17 at 00:27
  • Having said that, the stated behaviour looks completely normal to me. What you have here is a *user-unhandled* exception (not to be confused with *unobserved* exception), causing the debugger to break. If you were to run the program built in debug without actually attaching a debugger, it should behave exactly as you expect. – Kirill Shlenskiy Aug 05 '17 at 00:33
  • *"If you were to run the program built in debug.."* That actually seems to be right. However, having a behavior *while debugging* which is different than the expected behavior in the final build would cause serious confusion, and make debugging a much harder job! – 41686d6564 stands w. Palestine Aug 05 '17 at 00:42

2 Answers2

4

To expand upon our discussion in the comments:

What you have here is a user-unhandled exception (not to be confused with unobserved exception) causing the debugger to break. If you were to run the program built in debug without actually attaching a debugger, it should behave exactly as you expect. The continuation will run, and it will observe the exception from the antecedent task.

From your perspective, you are handling the exception, and if you were to write some vanilla synchronous code such as this:

try
{
    throw new Exception("Oops");
}
catch
{
}

... then the debugger is smart enough to work out that the exception is, indeed, handled, and treats it as such.

However, when you're dealing with task continuations, there are no similarly strong guarantees that your exception handling code will run. It can run on the same thread or a different thread, synchronously or asynchronously, or even not at all if the continuation fails to run (which can happen for a number of reasons that the runtime doesn't necessarily have control over). So the safe choice is for the debugger to say "I can't see this exception being handled anywhere in the immediate call stack, therefore it's user-unhandled".

To drive this point home, think about unobserved task exceptions. In .NET 4.0 they could tear down your program a solid minute after actually being thrown. That is how long it took for the runtime to work out with reasonable confidence that no task continuations actually looked at the exception. When you're debugging, you can't wait that long. If something looks unhandled, the safe choice is for the debugger to break immediately.

Finally, I should note that you can modify this behaviour by telling Visual Studio's debugger not to break on particular exception types (OperationCanceledException would be a good candidate as it comes up in asynchronous code a lot) via Debug -> Windows -> Exception Settings.

Kirill Shlenskiy
  • 9,367
  • 27
  • 39
  • I'm actually new to tasks. Thank you for the useful information. *"That is how long it took for the runtime to work out with reasonable confidence that no task continuations actually looked at the exception"* Doesn't the task complete *immediately* if an exception occurs within it? Why does it take so long? Maybe because the task continuations could execute other code before handling the exception? And could it still take all that time when running without the debugger (on .NET 4.5.2). – 41686d6564 stands w. Palestine Aug 05 '17 at 02:19
  • @AhmedAbdelhameed, your thought pattern is roughly right. It can take some time for the continuation to query the antecedent task's `Exception` (thereby marking it as "observed"). That's why `UnobservedTaskException` is not thrown immediately. Whether or not the antecedent task completed immediately after the exception was thrown is not really relevant as continuations are not guaranteed to *start* immediately following the antecedent task's completion due to the fundamentally asynchronous nature of the scheduler. – Kirill Shlenskiy Aug 05 '17 at 02:50
  • The debugger seems to rely solely on `try/catch` blocks to mark exceptions as "user-handled". It has no notion of continuations querying their antecedent task's `Exception` property or completion status. Nor should it, because its job is to alert you as soon as possible if it sees an exception, and that's simply not achievable with task continuations (or asynchronous code in general). – Kirill Shlenskiy Aug 05 '17 at 02:51
  • Okay, I totally understand now. Can you please clarify this last thing: Could the runtime still take that much time to check if the exception was handled by a task continuations *when running without attaching the debugger*? Because if yes, then handling task exception *at least using this way* isn't a good practice at all! Also if the answer is no, Could you refer to the reason please? Thank you! – 41686d6564 stands w. Palestine Aug 05 '17 at 12:05
  • @AhmedAbdelhameed, you are safe handling exceptions by means of continuations (refer to "Observing Exceptions by Using the Task.Exception Property" in the link posted by Dan D). Before `async/await` came about this was the preferred way to handle `Task` exceptions, as per Microsoft's documentation. You should only expect a noticeable delay in executing the exception handling continuation if your thread pool is saturated with work. Under normal circumstances, however, you can have a reasonable degree of confidence in your error handling continuations. – Kirill Shlenskiy Aug 05 '17 at 14:11
  • 1
    I mentioned `UnobservedTaskException` in my previous comments. This used to be a critical exception in .NET 4.0, which would tear down your application when detected. It was generally raised *significantly later* than the task exception itself. This is no longer a critical event as of .NET 4.5, so you do not need to worry about your process being torn down if you're taking too long to handle your exceptions (which, as I mentioned earlier, is unlikely to happen anyway unless you're absolutely trashing your thread pool). – Kirill Shlenskiy Aug 05 '17 at 14:12
  • Read more about the `UnobservedTaskException` event and escalation policy here: https://msdn.microsoft.com/en-us/library/system.threading.tasks.taskscheduler.unobservedtaskexception(v=vs.110).aspx – Kirill Shlenskiy Aug 05 '17 at 14:13
1

A Task that has Continuation that throws an exception will have the Exception wrapped as an AggregateException. If you check the InnerException you will see it contains "Oops"

See Exception Handling in TPL for more information

Dan D
  • 2,493
  • 15
  • 23
  • That's actually what I was expecting but as I stated, the exception never gets handled. In other words, the code in `ContinueWith` never gets executed. – 41686d6564 stands w. Palestine Aug 04 '17 at 22:46
  • I put together a quick fiddle, the console should show that the Continuation was executed. If you extract this code from everything else does it run as expected? Potential deadlock? https://dotnetfiddle.net/fM3rsP – Dan D Aug 04 '17 at 22:50
  • Your code runs on dotnetfiddle as expected, but when I copied it into an *empty* console application in VS, [the exception is still not handled!](https://i.stack.imgur.com/bAYxE.png) I'm using VS 2015, and .NET 4.5.2. – 41686d6564 stands w. Palestine Aug 04 '17 at 23:01
  • Not sure man, I've used .Net Fiddle, LinqPad, VS2015 and VS2017 with .NET 4.5.2 and I get console output as expected. – Dan D Aug 04 '17 at 23:06
  • This is very weird, because I just tested it with VS2010, and even tested it with VS2015 *on another computer*, and still have the same issue! – 41686d6564 stands w. Palestine Aug 04 '17 at 23:09
  • Looks like this behavior only occurs if the code is not optimized (Debug configuration). I've edited the question. – 41686d6564 stands w. Palestine Aug 04 '17 at 23:58