How to get detailed error reports on unhandled (and forced) exceptions in WPF (Updated)

UPDATE: The post has been updated since first being published with some improvements to the code.
UPDATE #2 (May 15, 2013): The post has been updated again with much cleaner code and additional functionality.

To err is human. And so, many (well, hopefully, not that many) times your programs may crash. And most of the time, they won’t crash inside your development environment, hooked to a debugger that will give you all the information you need on why and where the error happened. They’ll crash on your client’s machine. So, what can you do if you’re developing a WPF application, and want your program to produce nice, detailed error reports that the users can send to you?

It’s quite easy, actually.

First, go to your App.xaml.cs (or the equivalent in the language you’re using) file, and add the following code:

public const string AppName = "Your App Name";
public static readonly string AppDocsPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments)
											+ @"\Your App Docs Folder\";

/// <summary>
///     Handles the DispatcherUnhandledException event of the App control. Makes sure that any unhandled exceptions produce an error
///     report that includes a stack trace.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">
///     The <see cref="DispatcherUnhandledExceptionEventArgs" /> instance containing the event data.
/// </param>
private void app_DispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
{
	var exceptionString = e.Exception.ToString();
	var innerExceptionString = e.Exception.InnerException == null
								   ? "No inner exception information."
								   : e.Exception.InnerException.Message;

	prepareErrorReport(exceptionString, innerExceptionString, "Unhandled Exception");

	// Prevent default unhandled exception processing
	e.Handled = true;

	Environment.Exit(-1);
}

/// <summary>Forces a critical error to happen and produces an error-report which includes the stack trace.</summary>
/// <param name="e">The exception.</param>
/// <param name="additional">Any additional information provided by the developer.</param>
public static void ErrorReport(Exception e, string additional = "")
{
	var exceptionString = e.ToString();
	var innerExceptionString = e.InnerException == null ? "No inner exception information." : e.InnerException.Message;

	prepareErrorReport(exceptionString, innerExceptionString, additional);

	Environment.Exit(-1);
}

/// <summary>
/// Prepares an error report and saves it to a text file, or shows it on-screen if creating the file fails.
/// </summary>
/// <param name="exceptionString">The exception information (usually provided by ex.ToString()).</param>
/// <param name="innerExceptionString">The inner exception information (if any).</param>
/// <param name="additional">Any additional information provided by the developer.</param>
private static void prepareErrorReport(string exceptionString, string innerExceptionString, string additional = "")
{
	var versionString = "Version " + Assembly.GetExecutingAssembly().GetName().Version;

	try
	{
		var errorReportPath = AppDocsPath + @"errorlog_unh.txt";
		var f = new StreamWriter(errorReportPath);

		f.WriteLine("Error Report for {0}", AppName);
		f.WriteLine(versionString);
		f.WriteLine();
		if (!String.IsNullOrWhiteSpace(additional))
		{
			f.WriteLine("Developer information: " + additional);
		}
		f.WriteLine("Exception information:");
		f.Write(exceptionString);
		f.WriteLine();
		f.WriteLine();
		f.WriteLine("Inner Exception information:");
		f.Write(innerExceptionString);
		f.Close();

		MessageBox.Show(
			AppName + " encountered a critical error and will be terminated.\n\n" + "An Error Log has been saved at \n"
			+ errorReportPath,
			AppName + " Error",
			MessageBoxButton.OK,
			MessageBoxImage.Error);

		Process.Start(errorReportPath);
	}
	catch (Exception ex)
	{
		var s = "Can't create errorlog!\nException: " + ex;
		s += ex.InnerException != null ? "\nInner Exception: " + ex.InnerException : "";
		s += "\n\n";
		s += versionString;
		s += "Exception Information:\n" + exceptionString + "\n\n";
		s += "Inner Exception Information:\n" + innerExceptionString;
		MessageBox.Show(s, AppName + " Error", MessageBoxButton.OK, MessageBoxImage.Error);
	}
}

What the above does is try to write a file that includes the version information for the application, the exception message and the stack trace, as well as include the message of any inner exception. It will also show an error message to the user mentioning that, as well as open that file with the default .txt handler so that the user doesn’t have to look for the file if they decide to send the error report to you. Of course, the errorReportPath variable can be set to whatever you like, but make sure to try and save to a folder that your program would have permissions to write to. If that fails, the error message will be shown on screen. The above code includes a ForceCriticalError function as well, which allows you to “crash” the program on your own, producing the same kind of information, plus giving you the option of including a custom additional string to the error report.

The stack trace will have much more information if you include the .pdb (Program Debug) files in the released package. For example, you’ll get the number of the line of code that caused the exception. Pretty nice stuff, and makes debugging much easier!

Of course, the above is an event handler, so we need to set it to handle something. We’ll take advantage of the DispatcherUnhandledException event. So, go to App.xaml, and add this line inside the Application tag:

DispatcherUnhandledException="app_DispatcherUnhandledException"

You can name the function whatever you like, but make sure you use the same name in the event assignment as well as its implementation (pretty much won’t compile if you don’t). Also, you can do much more with this, like use the default mailto: handler to set-up a mail to be sent to you, or allow it to be sent directly using FTP upload or an SMTP server.

You can also not do Environment.Exit(-1) and see if the error was minor enough for the application to continue to operate (or give the option to the user by using Abort/Ignore/Retry). However, whether you want the users to continue working after an unhandled exception is up to you. I’d rather fix any errors first, but then if the error happens after a lot of work on the user and before they were able to save it, they might get frustrated. Then again, if the error has corrupted the data, and the user decides to save, overwriting their previous file, it might have been better if they got to keep the previous, working save of their data. So, it’s up to you. As the MSDN article notes, you can check the Exception type and if it’s something as silly as “FileNotFoundException”, you can let the user try to continue working. Still, make sure they report any and all errors to you, because you definitely don’t want unhandled exceptions happening in any case, even if the program can recover from them.

Advertisements

2 thoughts on “How to get detailed error reports on unhandled (and forced) exceptions in WPF (Updated)

  1. Pingback: RegistryKey.GetValue() returns null on existing values | One day at a time

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s