10

I am trying to wrap the exceptions that can be thrown by an async task using ContinueWith(). If I just throw from the continuation action things seem to work, but my debugger claims the exception is unhandled. Am I doing something wrong or is this a Visual Studio problem? Is there a cleaner way to do this, or a way to work around my debugger stopping on what is ultimately a handled exception?

The test below passes and prints "caught wrapped exception as expected", but when I debug it the throw new CustomException line shows as "unhandled by user code".

var task = DoWorkAsync().ContinueWith(t => {
    throw new CustomException("Wrapped", t.Exception.InnerException);  // Debugger reports this unhandled
}, TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously);

try {
    task.Wait();
    Assert.Fail("Expected work to fail");
} catch (AggregateException ag) {
    if (!(ag.InnerException is CustomException))
        throw;
}
Console.WriteLine("Caught wrapped exception as expected");
svick
  • 236,525
  • 50
  • 385
  • 514
jtb
  • 889
  • 11
  • 27

2 Answers2

12

When "Just My Code" is enabled, Visual Studio in some cases will break on the line that throws the exception and display an error message that says "exception not handled by user code." This error is benign. You can press F5 to continue and see the exception-handling behavior that is demonstrated in these examples. To prevent Visual Studio from breaking on the first error, just uncheck the "Just My Code" checkbox under Tools, Options, Debugging, General.

From http://msdn.microsoft.com/en-us/library/dd997415.aspx

gqsmooth
  • 136
  • 1
  • 3
7

You don't appear to be "wrapping" the exceptions with a continuation, you seem to be throwing the exception in the continuation. If DoWorkAsync is what can throw an exception, I would "wrap" that in a continuation as follows:

DoWorkAsync().ContinueWith(t=>{
 Console.WriteLine("Error occurred: " + t.Exception);
}, TaskContinuationOptions.OnlyOnFaulted);

Alternatively, if you want to "handle" the exception outside the async method, you could do this:

var task = DoWorkAsync();

task.Wait();
if(task.Exception != null)
{
  Console.WriteLine("Error occurred: " + task.Exception);
}

If you want to transform the thrown exception, you could do something like this:

var task = DoWorkAsync().ContinueWith(t=>{
 if(t.Exception.InnerExceptions[0].GetType() == typeof(TimeoutException))
 {
     throw new BackoffException(t.Exception.InnerExceptions[0]);
 }
}, TaskContinuationOptions.OnlyOnFaulted);

And you could handle that BackoffException like this:

if(task.IsFaulted)
{
   Console.WriteLine(task.Exception.InnerExceptions[0]);
   // TODO: check what type and do something other than WriteLine.
}
Peter Ritchie
  • 35,463
  • 9
  • 80
  • 98
  • Key here, is you have to access the Exception property (or have the exception re-thrown within an AggregateException by accessing the Task.Result property (if you have a Task)... – Peter Ritchie Jul 26 '12 at 21:35
  • DoWorkAsync() can throw a bunch of different exceptions, and I'm just trying to translate them into a few higher-level "CustomException"s that will be less cumbersome to handle in the calling code. I don't actually know how to completely handle things now. So I'm looking to transform a failed Task into another failed Task where the type of the exception is different. – jtb Jul 26 '12 at 21:38
  • If you throw a CustomException, the TPL subsystem will just wrap that in the same type of AggregateException that would wrap the exception thrown from DoWorkAsync. – Peter Ritchie Jul 26 '12 at 21:40
  • i.e. I'm not seeing the benefit of "CustomException" if all it does is aggregate and AggregateException... – Peter Ritchie Jul 26 '12 at 21:41
  • The real version of DoWorkAsync() can throw DataServiceRequestException, DataServiceClientException, TimeoutException, etc. At different places inside those exception objects are status codes indicating the error type (HTTP codes: 400, 503, etc). So in this ContinueWith I want to transform these into something like BackoffException(int retryAfterSeconds) and DatastoreCommException so when I use the data store higher up in the code my exception handling is much simpler. – jtb Jul 26 '12 at 21:47
  • DoWorkAsync can only throw one exception (i.e. TPL will only catch one exception, then the method is no longer running). It will capture that in an AggregateException class. You access that AggregateException class through the Task.Exception property (you can test if an exception occurred with the IsFaulted property). You can look into the AggregateException class for those exceptions and throw a new exception (BackoffException) but the new task's Exception property is still going to be an AggregateException. – Peter Ritchie Jul 26 '12 at 21:55
  • ...The code looking for the BackoffException will still have to process an AggregateException... – Peter Ritchie Jul 26 '12 at 21:56
  • I have no problem with the AggregateException. So it's correct to throw BackoffException from inside the ContinueWith? If I ultimately access and handle the Exception property of the Task returned by ContinueWith, then why does the debugger report that throw as unhandled? – jtb Jul 26 '12 at 21:59
  • To process specific exception you should use AggregateException.Handle method [http://msdn.microsoft.com/en-us/library/system.aggregateexception.handle.aspx]. You seeing it in debugger as unhandled because debugger give you a chance to see the exception before catch block - it's just a visual representation. In runtime process exception will fall in catch block as you written in code. – Alex F Jul 29 '12 at 12:13