Introduction
This is a DLL that can be attached to any project to provide error reporting. It has been designed with error reporting for a managed code project in mind. This makes reference to dbghelp.dll and its MiniDumpWriteDump
function. Additionally it handles unhandled exceptions and threading exceptions. All this information is then displayed in a nice neat box for presentation to the user and for them to act on. Directions about what they should do can also be included in the box.
Background
After several hours of research, I finally found that the MiniDump
routines were of very limited usefulness to those of us that work in managed code. I found this very useful article and I decided that I would expand upon it:
After getting this to work and enhancing it with some TechNet articles that I found, I found that I couldn't use the MiniDump
files because they aren't compatible with managed code without sos.dll and a whole bunch of typing, and even then, still only in a very limited way.
I wanted to be able to see the stack and the line number of the error, so I explored the various variables involved with exception processing and extracted a whole bunch of useful information. I then wrapped it up in a class and put an interface on it that will export the minidump and also all sorts of other readable information.
One thing that I was also shooting for and achieved is for the code to resume after the error if at all possible. This works best when we break our code down into little routines, so if one fails the others have a hope of continuing. Additionally, multithreading our applications goes a long way to allowing an application to continue running even after a severe unhandled exception.
Using the Code - Main Thread
If you just add this DLL to your project, you can use it with just a few lines of code. There are only a few public
methods so as not to confuse things. I'll show you the code of the sample application to see how to use it.
This is a very simple demo application, it is commented so you can follow along what is going on. It shows a form with just two buttons, each that causes an exception, one handled the other unhandled. You'll notice that you can keep clicking the buttons, the whole program doesn't crash even though one of the errors is completely unhandled:
Public Class frmDemo
Private Sub frmDemo_Load(ByVal sender As Object, ByVal e As System.EventArgs) _
Handles Me.Load
ErrorHandler.ErrorReporter.BugReportPath = _
"mailto:support@me.net?subject=<subject>&body=<body>"
ErrorHandler.ErrorReporter.SubjectPreface = "Demo Application: "
ErrorHandler.ErrorReporter.IsRTF = False
ErrorHandler.ErrorReporter.Message = _
"This is a message that can be an RTF reference. " & _
"If you want to display an RTF reference here just set _
ErrorHandler.ErrorReporter.Message = My.Resources.[ResourceName]"
ErrorHandler.ErrorReporter.WindowHeight = 500
ErrorHandler.ErrorReporter.WindowWidth = 500
ErrorHandler.ErrorReporter.SendEmail = True
ErrorHandler.ErrorReporter.ToAddress = New String(0) {"support@me.net"}
ErrorHandler.ErrorReporter.BodyPreface = _
"Please include a detailed description of what you were doing when _
this error occurred:" & _
Environment.NewLine & Environment.NewLine & Environment.NewLine & _
"========================================" & Environment.NewLine
AddHandler AppDomain.CurrentDomain.UnhandledException, _
AddressOf ErrorHandler.ErrorProcesser.UnhandledException
AddHandler Application.ThreadException, _
AddressOf ErrorHandler.ErrorProcesser.UnhandledException
End Sub
Private Sub btnHandled_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles btnHandled.Click
Dim a As Integer = 8
Dim b As Integer = 0
Try
a = a / b
Catch ex As Exception
MessageBox.Show(ErrorHandler.ErrorProcesser.BuildExceptionMessage(ex))
End Try
End Sub
Private Sub btnUnhandled_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles btnUnhandled.Click
Dim a As Integer = 8
Dim b As Integer = 0
a = a / b
End Sub
End Class
Using The Code - BackgroundWorker
One thing to note about trying to handle exceptions in BackgroundWorker
routines is that you must prepend this to the module that handles the DoWork
event.
<System.Diagnostics.DebuggerNonUserCodeAttribute()>
In order to do this, I usually end up with code that looks something like this:
<system.diagnostics.debuggernonusercodeattribute() /> _
Private Sub workerMyLongTask_DoWork(ByVal sender As System.Object, _
ByVal e As System.ComponentModel.DoWorkEventArgs) _
Handles workerMyLongTask.DoWork
...
...
End Sub
Now, if you want to handle errors that occur in a BackgroundWorker
thread, you should put the error handling in RunWorkerCompleted
event handling routine. Any errors that occur in the DoWork
event handling routine are available as e.Error
in the DoWork
routine. In order to make sure errors are handled correctly when they occur in a BackgroundWorker
thread, code like the following tends to work very well:
Private Sub workerMyLongTask_RunWorkerCompleted(ByVal sender As Object, _
ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) _
Handles workerMyLongTask.RunWorkerCompleted
If e.Error IsNot Nothing Then
...
MessageBox.Show_
(ErrorHandler.ErrorProcesser.BuildExceptionMessage(e.Error))
...
Else
...
End If
End Sub
How the Code Works
The code that actually handles the application is contained in several modules which are broken down here. This first part is the basic declarations and such. The Enum
is based on information from Microsoft's own documentation:
Private Declare Function MiniDumpWriteDump Lib "dbghelp.dll" (
ByVal hProcess As IntPtr, ByVal ProcessId As Int32, ByVal hFile As IntPtr,
ByVal DumpType As MINIDUMP_TYPE, ByVal ExceptionParam As IntPtr,
ByVal UserStreamParam As IntPtr, ByVal CallackParam As IntPtr) As Boolean
Private Enum MINIDUMP_TYPE
MiniDumpNormal = 0
MiniDumpWithDataSegs = 1
MiniDumpWithFullMemory = 2
MiniDumpWithHandleData = 4
MiniDumpFilterMemory = 8
MiniDumpScanMemory = 10
MiniDumpWithUnloadedModules = 20
MiniDumpWithIndirectlyReferencedMemory = 40
MiniDumpFilterModulePaths = 80
MiniDumpWithProcessThreadData = 100
MiniDumpWithPrivateReadWriteMemory = 200
MiniDumpWithoutOptionalData = 400
MiniDumpWithFullMemoryInfo = 800
MiniDumpWithThreadInfo = 1000
MiniDumpWithCodeSegs = 2000
End Enum
This next part is the routine that calls the external function to generate the MiniDump
. It then returns the path where the MiniDump
was saved if the MiniDump
was successful:
Private Shared Function SaveMiniDump()
Using dumpProcess As System.Diagnostics.Process =
System.Diagnostics.Process.GetCurrentProcess
Dim strDumpFile As String = Path.ChangeExtension(
Path.GetTempFileName.ToString, ".mdmp")
Dim bolResult As Boolean
Using objDumpFile As FileStream = _
New FileStream(strDumpFile, FileMode.Create)
bolResult = MiniDumpWriteDump(dumpProcess.Handle, dumpProcess.Id,
objDumpFile.SafeFileHandle.DangerousGetHandle,
MINIDUMP_TYPE.MiniDumpWithDataSegs, 0, 0, 0)
End Using
If bolResult Then
Return strDumpFile
Else
Return ""
End If
End Using
End Function
Then we have the code designed specifically to handle unhandled exceptions and threading exceptions. It is an overloaded function to keep things easy on me. You'll also notice a few other things are included here that aren't part of the exception message. I was trying to figure out how to get local variables dumped here too, but to no avail (if you know how, please let me know and I'll update this code):
Public Shared Sub UnhandledException(ByVal sender As Object,
ByVal e As UnhandledExceptionEventArgs)
Dim strAdditionalInfo As String = ""
Dim strMiniDumpLocation As String = ""
strAdditionalInfo &= sender.ToString() & Environment.NewLine &
Environment.NewLine
strAdditionalInfo &= "Open Forms: "
For x = 0 To Application.OpenForms.Count - 1
strAdditionalInfo &= _
Application.OpenForms.Item(x).Name & Environment.NewLine
Next
strAdditionalInfo &= Environment.NewLine
strAdditionalInfo &= BuildExceptionMessage(e)
strMiniDumpLocation = SaveMiniDump()
ShowErrorForm(strAdditionalInfo, strMiniDumpLocation, e.ToString)
End Sub
Public Shared Sub UnhandledException(ByVal sender As Object,
ByVal e As System.Threading.ThreadExceptionEventArgs)
Dim strAdditionalInfo As String = ""
Dim strMiniDumpLocation As String = ""
strAdditionalInfo &= sender.ToString() & Environment.NewLine &
Environment.NewLine
strAdditionalInfo &= Application.ProductVersion & Environment.NewLine &
Environment.NewLine
strAdditionalInfo &= "Open Forms: "
For x = 0 To Application.OpenForms.Count - 1
strAdditionalInfo &= Application.OpenForms.Item(x).Name &
Environment.NewLine
Next
strAdditionalInfo &= Environment.NewLine
strAdditionalInfo &= BuildExceptionMessage(e)
strMiniDumpLocation = SaveMiniDump()
ShowErrorForm(strAdditionalInfo, strMiniDumpLocation, e.Exception.Message)
End Sub
The code to show the form. You'll notice that it passes the path to the MiniDump
file, the Error.Message
, and the additional information about the error to the form for the user to access:
Private Shared Sub ShowErrorForm(ByVal strAdditionalInfo As String,
ByVal strMiniDumpLocation As String, ByVal strErrorMessage As String)
Dim frmError As New ErrorReporter(strAdditionalInfo, strMiniDumpLocation,
strErrorMessage)
frmError.Show()
End Sub
Finally, the overloaded routines to collect all the data we can from the exception objects:
Public Shared Function BuildExceptionMessage(
ByVal ex As UnhandledExceptionEventArgs) As String
BuildExceptionMessage = ""
If ex.GetType IsNot Nothing Then BuildExceptionMessage &= "Type: " &
ex.GetType.ToString & Environment.NewLine & Environment.NewLine
If ex.ExceptionObject IsNot Nothing Then BuildExceptionMessage &=
"ExceptionObject: " & ex.ExceptionObject.ToString &
Environment.NewLine & Environment.NewLine
BuildExceptionMessage &= "IsTerminating: " & ex.IsTerminating.ToString &
Environment.NewLine & Environment.NewLine
BuildExceptionMessage &= "HashCode: " & ex.GetHashCode.ToString &
Environment.NewLine & Environment.NewLine
If ex.ToString IsNot Nothing Then BuildExceptionMessage &= "To String: " &
ex.ToString & Environment.NewLine & Environment.NewLine
End Function
Public Shared Function BuildExceptionMessage(
ByVal ex As System.Threading.ThreadExceptionEventArgs) As String
BuildExceptionMessage = ""
Dim x As Integer
If ex.GetType IsNot Nothing Then BuildExceptionMessage &= "Type: " &
ex.GetType.ToString & Environment.NewLine & Environment.NewLine
If ex.Exception.GetType IsNot Nothing Then BuildExceptionMessage &=
"Exception Type: " & ex.Exception.GetType.ToString & Environment.NewLine &
Environment.NewLine
If ex.Exception.Message IsNot Nothing Then BuildExceptionMessage &=
"Exception Message: " & ex.Exception.Message & Environment.NewLine &
Environment.NewLine
If ex.Exception.TargetSite IsNot Nothing Then BuildExceptionMessage &=
"Exception TargetSite: " & ex.Exception.TargetSite.ToString &
Environment.NewLine & Environment.NewLine
For x = 0 To ex.Exception.Data.Count - 1
BuildExceptionMessage &= "Exception Data " & x & ": " &
ex.Exception.Data.Item(x).ToString & Environment.NewLine
Next
If ex.Exception.Data.Count <> 0 Then BuildExceptionMessage &= Environment.NewLine
If ex.Exception.Data IsNot Nothing Then BuildExceptionMessage &= "Data: " &
ex.Exception.Data.ToString & Environment.NewLine & Environment.NewLine
If ex.Exception.Source IsNot Nothing Then BuildExceptionMessage &=
"Exception Source: " & ex.Exception.Source & Environment.NewLine &
Environment.NewLine
If ex.Exception.InnerException IsNot Nothing Then BuildExceptionMessage &=
"Exception InnerException: " & ex.Exception.InnerException.ToString &
Environment.NewLine & Environment.NewLine
If ex.Exception.StackTrace IsNot Nothing Then BuildExceptionMessage &=
"Exception StackTrace: " & ex.Exception.StackTrace & Environment.NewLine &
Environment.NewLine
If ex.Exception.GetBaseException IsNot Nothing Then BuildExceptionMessage &=
"Exception BaseException: " & ex.Exception.GetBaseException.ToString &
Environment.NewLine & Environment.NewLine
BuildExceptionMessage &= "Exception HashCode: " & ex.GetHashCode.ToString &
Environment.NewLine & Environment.NewLine
If ex.ToString IsNot Nothing Then BuildExceptionMessage &=
"Exception ToString: " & ex.ToString & Environment.NewLine &
Environment.NewLine
BuildExceptionMessage &= "GetHashCode: " & ex.GetHashCode.ToString &
Environment.NewLine & Environment.NewLine
If ex.GetType IsNot Nothing Then BuildExceptionMessage &= "GetType: " &
ex.GetType.ToString & Environment.NewLine & Environment.NewLine
If ex.ToString IsNot Nothing Then BuildExceptionMessage &= "To String: " &
ex.ToString & Environment.NewLine & Environment.NewLine
End Function
Public Shared Function BuildExceptionMessage(ByVal ex As System.Exception) As String
BuildExceptionMessage = ""
Dim x As Integer
If ex.GetType IsNot Nothing Then BuildExceptionMessage &= "Type: " &
ex.GetType.ToString & Environment.NewLine & Environment.NewLine
If ex.GetType IsNot Nothing Then BuildExceptionMessage &= "Exception Type: " &
ex.GetType.ToString & Environment.NewLine & Environment.NewLine
If ex.Message IsNot Nothing Then BuildExceptionMessage &= "Exception Message: " &
ex.Message & Environment.NewLine & Environment.NewLine
If ex.TargetSite IsNot Nothing Then BuildExceptionMessage &=
"Exception TargetSite: " & ex.TargetSite.ToString & Environment.NewLine &
Environment.NewLine
For x = 0 To ex.Data.Count - 1
BuildExceptionMessage &= "Exception Data " & x & ": " &
ex.Data.Item(x).ToString & Environment.NewLine
Next
If ex.Data.Count <> 0 Then BuildExceptionMessage &= Environment.NewLine
If ex.Data IsNot Nothing Then BuildExceptionMessage &= "Data: " &
ex.Data.ToString & Environment.NewLine & Environment.NewLine
If ex.Source IsNot Nothing Then BuildExceptionMessage &=
"Exception Source: " & ex.Source & Environment.NewLine & Environment.NewLine
If ex.InnerException IsNot Nothing Then BuildExceptionMessage &=
"Exception InnerException: " & ex.InnerException.ToString &
Environment.NewLine & Environment.NewLine
If ex.StackTrace IsNot Nothing Then BuildExceptionMessage &=
"Exception StackTrace: " & ex.StackTrace & Environment.NewLine &
Environment.NewLine
If ex.GetBaseException IsNot Nothing Then BuildExceptionMessage &=
"Exception BaseException: " & ex.GetBaseException.ToString &
Environment.NewLine & Environment.NewLine
BuildExceptionMessage &= "Exception HashCode: " & ex.GetHashCode.ToString &
Environment.NewLine & Environment.NewLine
If ex.ToString IsNot Nothing Then BuildExceptionMessage &=
"Exception ToString: " & ex.ToString & Environment.NewLine &
Environment.NewLine
BuildExceptionMessage &= "GetHashCode: " & ex.GetHashCode.ToString &
Environment.NewLine & Environment.NewLine
If ex.GetType IsNot Nothing Then BuildExceptionMessage &= "GetType: " &
ex.GetType.ToString & Environment.NewLine & Environment.NewLine
If ex.ToString IsNot Nothing Then BuildExceptionMessage &= "To String: " &
ex.ToString & Environment.NewLine & Environment.NewLine
End Function
The code that drives the form is really pretty simple and well commented. There is one function that deserves a little explanation though. The following routine executes when the Report Bug button is pressed. It will determine if the action to take is to SendMail
or to Process.Start
based on variables that have been set. If the action is SendMail
then the module found over here is used to invoke MAPI32
to open the default email client and populate fields appropriately.
I tried to get the SendToMAPI
routine to work in a different thread but have found that for some reason, it won't work. If you have any suggestions, I'd be happy to implement them.
Private Sub btnBug_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles btnBug.Click
If SendEmail Then
Dim myMessage As New SendToMAPI.SendToMAPI.MAPI
Dim strAddress As String
If IncludeDumpAttachment Then
myMessage.AddAttachment(_strMiniDumpLocation)
End If
If ToAddress IsNot Nothing Then
For Each strAddress In ToAddress
myMessage.AddRecipientTo(strAddress)
Next
End If
If CCAddress IsNot Nothing Then
For Each strAddress In CCAddress
myMessage.AddRecipientCC(strAddress)
Next
End If
If BCCAddress IsNot Nothing Then
For Each strAddress In BCCAddress
myMessage.AddRecipientBCC(strAddress)
Next
End If
myMessage.SendMailPopup(SubjectPreface & _strErrorMessage, _
BodyPreface & _strAdditionalInfo)
Else
Dim strBugReportPathModified As String = BugReportPath
strBugReportPathModified = strBugReportPathModified.Replace_
("<subject />", SubjectPreface & _strErrorMessage)
strBugReportPathModified = strBugReportPathModified.Replace_
("", BodyPreface & _strAdditionalInfo)
System.Diagnostics.Process.Start(strBugReportPathModified)
Me.WindowState = FormWindowState.Minimized
End If
End Sub
Points of Interest
One important thing to note about this is that almost everything is a Public Shared
or Private Shared
routine or variable. This is because the handlers for an unhandled exception or a threading exception must be Public Shared
. Thus, these classes shouldn't be instantiated as is correctly shown in the sample application.
History
- 22nd October, 2009: The code was dramatically updated to support much better emailing of errors. An attempt was made at making it multi-threaded but due to the new way email is processed, it was not possible.
- 17th February, 2009: This second revision has added an explanation on how to handle errors generated in
BackgroundWorker
threads. - 9th February, 2009: This is the first revision to this code and is very basic.
It is designed to return at least some data for when a user crashes your application so that you can see a little more of what happened. If suggestions are given on how to expand this functionality, I will gladly consider them for implementation.