Async methods failures can be hard to diagnose
Using asynchronous programming can make your application be more responsive and seem faster. However, beware of possible failures that are hard to diagnose. I’ve just been analyzing some of these crash reports that are sent to Microsoft from the Problem Reporting Settings (below).
A big part of writing programs is calling other programs, or APIs, to do various things. Some of these may throw exceptions if passed invalid parameters. These parameters might come from user input or even the user OS version (the valid characters in a file system can vary). An API might be called to operate on something that no longer exists (System.Diagnostics.Process.GetProcessById) , perhaps due to a race condition. An application might be running in an operating system locale where the Date/Time formats or numeric separators might be different, causing exceptions.
When these exceptions occur in an asynchronous method, the call stacks of the crashes can be hard to decipher.
Many of these failures come from code that doesn’t come from Visual Studio, but from a VS extension running in the VS process, perhaps from a 3rd party.
The sample below produces the same call stack as these crashes.
Run the application below:
1. Start Visual Studio
2. File->New->Project->C# WPF Application
3. Paste the code below into MainWindow.xaml.cs
4. Hit Ctrl F5 to run the program without debugging it.
5. Click the Doit Button.
The button click code queues an asynchronous work item that fetches some web content. After the content is fetched, “DoThePossibleCrash” is called. It just calls Path.GetFileName on a passed in string. If the checkbox is unchecked, the passed in string is valid and the web content is returned as a string and displayed in the textbox. If the crash checkbox is checked, the string passed contains some invalid characters, like “<>”, which causes the GetFileName call to throw an exception, which is uncaught and thus crashes the program.
Cancel or Close the program.
If your program just disappears instead of showing the crash dialog, check your settings and this registry key:
HKEY_CURRENT_USER\Software\Microsoft\Windows\Windows Error Reporting “DontShowUI”
Now, in VS, hit F5 to run the code under the VS debugger:
Make sure the CLR Exceptions are not caught in the debugger:
Also, disable JustMyCode in Settings->Debugging.
The call stack from the VS debugger shows:
mscorlib.dll!System.Runtime.CompilerServices.AsyncMethodBuilderCore.ThrowAsync.AnonymousMethod__5(object state) Line 1010 C#
mscorlib.dll!System.Threading.QueueUserWorkItemCallback.WaitCallback_Context(object state) Line 1283 C#
mscorlib.dll!System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx) Line 581 C#
mscorlib.dll!System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext executionContext, System.Threading.ContextCallback callback, object state, bool preserveSyncCtx) Line 531 C#
mscorlib.dll!System.Threading.QueueUserWorkItemCallback.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem() Line 1261 C#
mscorlib.dll!System.Threading.ThreadPoolWorkQueue.Dispatch() Line 829 C#
> mscorlib.dll!System.Threading._ThreadPoolWaitCallback.PerformWaitCallback() Line 1170 C#
Using WinDBG sometimes I get a more complete callstack showing the code that causes the exception.
EXCEPTION_MESSAGE: Illegal characters in path.
MANAGED_OBJECT_NAME: SYSTEM.ARGUMENTEXCEPTION
MANAGED_STACK_COMMAND: !pe 245c584
LAST_CONTROL_TRANSFER: from 720121a7 to 764f2c1a
PRIMARY_PROBLEM_CLASS: WRONG_SYMBOLS
BUGCHECK_STR: APPLICATION_FAULT_WRONG_SYMBOLS_CLR_EXCEPTION
STACK_TEXT:
0802e9f4 71b9755f mscorlib_ni!System.IO.Path.CheckInvalidPathChars+0xa995bf
0802ea04 71112cb6 mscorlib_ni!System.IO.Path.GetFileName+0x16
0802ea1c 00801887 wpfcrashasync!WpfApplication1.MainWindow.DoThePossibleCrash+0x11f
0802ea64 00800e74 wpfcrashasync!WpfApplication1.MainWindow+_AccessTheWebAsync_d__e.MoveNext+0x20c
0802e73c 7117d17a mscorlib_ni!System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess+0x5e
0802e74c 7117d115 mscorlib_ni!System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification+0x35
0802e758 718f7cf9 mscorlib_ni!System.Runtime.CompilerServices.TaskAwaiter_1[[System.__Canon,_mscorlib]].GetResult+0x19
0802e760 008009df wpfcrashasync!WpfApplication1.MainWindow+_doTheWeb_d__b.MoveNext+0x1ff
07d8f698 718f4633 mscorlib_ni!System.Runtime.CompilerServices.AsyncMethodBuilderCore._ThrowAsync_b__5+0x33
07d8f6a0 7114c6a6 mscorlib_ni!System.Threading.QueueUserWorkItemCallback.WaitCallback_Context+0x3e
07d8f6a8 71113207 mscorlib_ni!System.Threading.ExecutionContext.RunInternal+0xa7
07d8f714 71113156 mscorlib_ni!System.Threading.ExecutionContext.Run+0x16
07d8f728 7116b910 mscorlib_ni!System.Threading.QueueUserWorkItemCallback.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem+0x60
07d8f73c 7116b10a mscorlib_ni!System.Threading.ThreadPoolWorkQueue.Dispatch+0x10a
07d8f78c 7116aff5 mscorlib_ni!System.Threading._ThreadPoolWaitCallback.PerformWaitCallback+0x5
https://msdn.microsoft.com/en-us/magazine/jj991977.aspx
see also:
Scan the Windows Event Log for your application crashes and hangs
<code>
//#define UseTryCatch // uncomment this line to try with Exception handling
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using System.Collections;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
namespace WpfApplication1
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
TextBox _txtBoxResults;
public MainWindow()
{
InitializeComponent();
this.Loaded += (o, e) =>
{
this.Width = 1000;
this.Height = 800;
var sp = new StackPanel() { Orientation = Orientation.Vertical };
var chkbox = new CheckBox() { Content = "Cause Crash" };
var btn = new Button() { Content = "_DoIt" };
sp.Children.Add(chkbox);
sp.Children.Add(btn);
_txtBoxResults = new TextBox();
sp.Children.Add(_txtBoxResults);
this.Content = sp;
btn.Click += (ob, eb) =>
{
ThreadPool.QueueUserWorkItem(doTheWeb, chkbox.IsChecked);
//var res = DoThePossibleCrash(@"c:\SomeDirName\SomeFile<badchars>.txt");
//_txtBox.Text = res;
};
};
}
async void doTheWeb(object o)// the param we pass is the checkbox.IsChecked
{
var urlContent = await AccessTheWebAsync((bool)o);
_txtBoxResults.Dispatcher.Invoke(() => _txtBoxResults.Text = urlContent);
}
// Three things to note in the signature:
// - The method has an async modifier.
// - The return type is Task or Task<T>. (See "Return Types" section.)
// Here, it is Task<int> because the return statement returns an integer.
// - The method name ends in "Async."
async Task<string> AccessTheWebAsync(bool fCauseACrash)
{
var urlContents = string.Empty;
#if UseTryCatch
try
{
#endif
// You need to add a reference to System.Net.Http to declare client.
HttpClient client = new HttpClient();
Task<string> getWebContentTask =
client.GetStringAsync("https://blogs.msdn.com/b/calvin_hsia/archive/2013/07/31/10438571.aspx");
// The await operator suspends AccessTheWebAsync.
// - AccessTheWebAsync can't continue until getStringTask is complete.
// - Meanwhile, control returns to the caller of AccessTheWebAsync.
// - Control resumes here when getStringTask is complete.
// - The await operator then retrieves the string result from getStringTask.
urlContents = await getWebContentTask;
var testString = @"\foobar";
if (fCauseACrash)
{
testString += "<temp>"; //illegal chars
}
// Call stacks can contain strings
//that should not be treated with invalid chars like "<"
// f:\dd\internalapis\atlmfc\inc\atlcom.h(2973) : msenv.dll!ATL::CComObject<CTaskList>::CreateInstance + 143 bytes
DoThePossibleCrash(testString, fValidateString: false);
// another System.Argument exception crash:
// get the ProcessId of a non-existent process:
//var x = System.Diagnostics.Process.GetProcessById(222222);
// yet another System.Argument exception crash: An item with the same key has already been added
//var dict = new Dictionary<int, int>();
//dict[1] = 1;
//dict.Add(1, 1);
#if UseTryCatch
}
catch (System.Exception ex)
{
// handle the exception!
urlContents = ex.ToString();
}
#endif
return urlContents;
}
string DoThePossibleCrash(string testString, bool fValidateString)
{
var retVal = string.Empty;
var isValid = true;
if (fValidateString)
{
var invalidChars = Path.GetInvalidPathChars();
//caution: MSDN says even this set of chars is not guaranteed to be complete
if (testString.Any(c => invalidChars.Contains(c)))
{
retVal = "Invalid characters in path";
isValid = false;
}
}
if (isValid)
{
retVal = Path.GetFileName(testString); // cause an argument exception: Invalid Chars in path
}
return retVal;
}
}
}
</code>