Tuesday, November 12, 2013

Async / await for .NET Framework 4 and Stack Trace

If you find yourself wanting to use the async / await language of .NET Framework 4.5 but still need to target Windows XP, what you need is called Microsoft Async Targeting Pack.

The last .NET Framework that is supported on Windows XP is version 4. To allow for the new async / await features in .NET Framework 4, Microsoft came out with an “extension” to the Framework called Microsoft Async Targeting Pack. It is basically a set of DLLs which you can get as a NuGet package.

There are two packages you can choose from. The original one is called Async Targeting Pack for Visual Studio 11, which was later superseded by Async for .NET Framework 4, Silverlight 4 and 5, and Windows Phone 7.5 and 8.

If you go with the new one, as I originally did with my applications, sooner or later you hit one problem. Whenever an exception is thrown in a an async code, you will not get a stack trace information you would expect. Consider the following example:

private async void Button1_Click(object sender, RoutedEventArgs e)
{
try
{
await DoSomethingAsync();
}

catch (Exception ex)
{
Debug.Write(ex.StackTrace);
}
}

public async Task DoSomethingAsync()
{
await TaskEx.Run(() =>
{
throw new ApplicationException("My Application Exception");
});
}

The code is relatively straightforward. You click a button which runs an async method. This method throws an exception in its body. Once the exception propagates back to the click event handler, you catch it and write the stack trace information in the debug output.

If you run the code with Async for .NET Framework 4, Silverlight 4 and 5, and Windows Phone 7.5 and 8, you get the following output:

at Microsoft.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at Microsoft.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccess(Task task)
at Microsoft.Runtime.CompilerServices.TaskAwaiter.ValidateEnd(Task task)
at Microsoft.Runtime.CompilerServices.TaskAwaiter.GetResult()
at WpfApplication1.MainWindow.<Button1_Click>d__0.MoveNext() in c:\Data\SkyDrive\Development\Projects\Test\WpfApplication1a\WpfApplication1\MainWindow.xaml.cs:line 34

The only thing you get to know is that the error was caught at line 34. There is no information about the real exception source at line 47. As you can imagine, this can easily make the debugging of your applications a nightmare.

Searching the web, I found some explanations as to why this is happening, what stack trace is and is not supposed to show, etc. I even found a few workarounds (a custom code you need to write in order to get the information you would expect), but that was not a way I wanted to go.

Later I found a much simpler solution. It turns out that if you run the code with the “earlier” or “older” version of the async targeting pack, the Async Targeting Pack for Visual Studio 11, the same code produces much friendlier stack trace:

Server stack trace: 
at WpfApplication1.MainWindow.<DoSomethingAsync>b__3() in c:\Data\SkyDrive\Development\Projects\Test\WpfApplication1a\WpfApplication1\MainWindow.xaml.cs:line 47
at System.Threading.Tasks.Task`1.InnerInvoke()
at System.Threading.Tasks.Task.Execute()

Exception rethrown at [0]:
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.ValidateEnd(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
at WpfApplication1.MainWindow.<DoSomethingAsync>d__5.MoveNext() in c:\Data\SkyDrive\Development\Projects\Test\WpfApplication1a\WpfApplication1\MainWindow.xaml.cs:line 45

Exception rethrown at [1]:
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.ValidateEnd(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
at WpfApplication1.MainWindow.<Button1_Click>d__0.MoveNext() in c:\Data\SkyDrive\Development\Projects\Test\WpfApplication1a\WpfApplication1\MainWindow.xaml.cs:line 34

Now, that is the way to go!

There is one more gotcha, however. If you’re trying to install the Async Targeting Pack for Visual Studio 11 via Manage NuGet Packages tool in Visual Studio, you will not be able to find the package. This has been done by Microsoft so that people will always install the newer version if the are getting the package for the fist time. The older version of the package however is still available. To get it, you need to use the Package Manager Console and enter the following command:

PM> Install-Package Microsoft.CompilerServices.AsyncTargetingPack -Version 1.0.1

And that should take care of the stack trace problem!

For completeness, if you run our example code under .NET Framework 4.5, which has a native support for async / await, you get a similar output, telling you all you need to know about the exception:

    at WpfApplication1.MainWindow.<DoSomethingAsync>b__3() in c:\Data\SkyDrive\Development\Projects\Test\WpfApplication1a\WpfApplication1\MainWindow.xaml.cs:line 47
at System.Threading.Tasks.Task`1.InnerInvoke()
at System.Threading.Tasks.Task.Execute()
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at WpfApplication1.MainWindow.<DoSomethingAsync>d__5.MoveNext() in c:\Data\SkyDrive\Development\Projects\Test\WpfApplication1a\WpfApplication1\MainWindow.xaml.cs:line 45
--- End of stack trace from previous location where exception was thrown ---
at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at WpfApplication1.MainWindow.<Button1_Click>d__0.MoveNext() in c:\Data\SkyDrive\Development\Projects\Test\WpfApplication1a\WpfApplication1\MainWindow.xaml.cs:line 34

That is - the good new is that it has been fixed in .NET Framework 4.5.

No comments:

Post a Comment