AsyncStackTraceEx

Normally when you look at Exception.StackTrace, it looks pretty bad for async method calls, and doesn't even include line numbers when it's run on a device like Phone which lacks PDBs. This package provides an alternative, Exception.StackTraceEx(), which has cleaned up async output, and which gives line and file numbers even on phone. This way, if your app crashes, you can synthesize much better crash reports for the user to email back to you. Await DownloadAsync(url).Log() To get that richer information in exception stack-traces, you have to augment your "await expr" calls with "await expr.Log()", as shown above. Await DownloadAsync(url).Log("DownloadAsync",url) If you chose to include more information, then the exception stack traces you get will be even richer and more actionable.


Keywords
exception, async, stacktrace
License
MIT
Install
Install-Package AsyncStackTraceEx -Version 1.0.1.1

Documentation

async-exception-stacktrace

(AsyncStackTraceEx is available on NuGet)

Scenario: I've written my app and released it to users. Some of them have reported crashes but I don't know where. So I released an update which captures unhandled exceptions and invites the users to email these to me, in the hope that I can figure out what's wrong.

Problem: On Windows Phone it's not possible to deploy the PDB alongside the EXE/DLL. Without it, Exception.StackTrace is unable to provide line-numbers, and so all my phone error-reports come back without line-numbers and I don't know how to debug.

Solution: I'll write a small helper which adds those line numbers to exception stack-traces, without needing PDBs.

How it works

Here's a comparison of the output.

Using the traditional PDB-based Exception.StackTrace (the line numbers parts are absent on phone since they depend on PDBs)

  at VB$StateMachine_3_BarAsync.MoveNext() ~~in Class1.vb:line 24~~
--- End of stack trace from previous location where exception was thrown ---
at TaskAwaiter.ThrowForNonSuccess(Task task)
  at TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
at TaskAwaiter.GetResult()
  at VB$StateMachine_2_FooAsync.MoveNext() ~~in Class1.vb:line 19~~
--- End of stack trace from previous location where exception was thrown ---
  at TaskAwaiter.ThrowForNonSuccess(Task task)
  at TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
  at TaskAwaiter.GetResult()
  at VB$StateMachine_1_TestAsync.MoveNext() ~~in Class1.vb:line 14~~
--- End of stack trace from previous location where exception was thrown ---
   at TaskAwaiter.ThrowForNonSuccess(Task task)
   at TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at TaskAwaiter`1.GetResult()
   at VB$StateMachine_0_Button1_Click.MoveNext() ~~in Class1.vb:line 5 ~~
   at VB$StateMachine_3_BarAsync.MoveNext() ~~in Class1.vb:line 24~~
--- End of stack trace from previous location where exception was thrown ---
   at TaskAwaiter.ThrowForNonSuccess(Task task)
   at TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at TaskAwaiter.GetResult()
   at VB$StateMachine_2_FooAsync.MoveNext() ~~in Class1.vb:line 19~~
--- End of stack trace from previous location where exception was thrown ---
   at TaskAwaiter.ThrowForNonSuccess(Task task)
   at TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at TaskAwaiter.GetResult()
   at VB$StateMachine_1_TestAsync.MoveNext() ~~in Class1.vb:line 14~~
--- End of stack trace from previous location where exception was thrown ---
   at TaskAwaiter.ThrowForNonSuccess(Task task)
   at TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at TaskAwaiter`1.GetResult()
   at VB$StateMachine_0_Button1_Click.MoveNext() ~~in Class1.vb:line 5~~

Using this package, Exception.StackTraceEx (the line numbers parts are present even on Phone)

   at Test.BarAsync
   at Test.FooAsync()#BarAsync in Class1.vb:19
   at Test.TestAsync()#FooAsync(True) in Class1.vb:14
   at Test.Button1_Click() in Class1.vb:5 

I noticed that all the exceptions I ever cared about either arose in async operations in the framework, or in my own async methods. That let me use a fluent syntax, like this: (I've given the examples in C#, to balance out the fact that implementation is in VB...)

private async void Button1_Click(object sender, RoutedEventArgs e)
{
    try
    {
        await TestAsync().Log();    // so exception's StackTraceEx will show "Button1Click() in MainPage.xaml.cs:53"
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.StackTraceEx());  // this retrieves the version of the stacktrace that includes line numbers
    }
}

Whenever I await something, I stick on .Log() at the end. This ensures that line numbers are preserved for any exception that comes out of the awaited task. And rather than retrieving the Exception.StackTrace property, I retrieve the stack-trace through my Exception.StackTraceEx() extension method. This cleans up the async callstack into something more readable, and inserts back all those line numbers. I also added two other optional overloads of await-point logging, to get richer information in the exception's async callstack; I use that richer information to (optionally) indicate which API I'm calling, and what arguments I'm passing to it. After all, it's often the arguments to a method that are crucial to understanding why it threw the exception.

await BarAsync(b).Log("BarAsync");           // so the exception stacktrace shows "...#BarAsync in MainPage.xaml.cs:61"
await FooAsync(true).Log("FooAsync", true);  // so the exception stacktrace shows "...#FooAsync(true) in MainPage.xaml.cs:72"