Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / VB

Managed Code Error Reporting

4.29/5 (4 votes)
23 Oct 2009CPOL5 min read 27.3K   237  
Module to generate a MiniDump on unhandled errors and allow the user to handle the error.
ErrorBox.JPG

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:

VB.NET
Public Class frmDemo

    Private Sub frmDemo_Load(ByVal sender As Object, ByVal e As System.EventArgs) _
	Handles Me.Load
        'optional, but highly suggested parameters to set

        'could be any process that you want to kick off, web address, 
        'basically anything you can type in a 'run' box
        ErrorHandler.ErrorReporter.BugReportPath = _
		"mailto:support@me.net?subject=<subject>&body=<body>"
        ErrorHandler.ErrorReporter.SubjectPreface = "Demo Application: "
        'either true or false depending on if the Message is 
        'plain text or is in RTF format
        ErrorHandler.ErrorReporter.IsRTF = False
        'either plain text or rich text
        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]"
        'optionally set the window dimensions (default: 600x370)
        ErrorHandler.ErrorReporter.WindowHeight = 500
        ErrorHandler.ErrorReporter.WindowWidth = 500
        'Use MAPI to send a message (this make the above BugReportPath unused)
        ErrorHandler.ErrorReporter.SendEmail = True
        ErrorHandler.ErrorReporter.ToAddress = New String(0) {"support@me.net"}
        'Specify the preface to the body
        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

        'add in the handlers for the unhandled exceptions
        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
            'handled 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

        'unhandled exception
        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.

VB.NET
<System.Diagnostics.DebuggerNonUserCodeAttribute()>

In order to do this, I usually end up with code that looks something like this:

VB.NET
<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:

VB.NET
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:

VB.NET
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
    'Include just the information necessary to capture stack traces for all
    'existing threads in a process.
    MiniDumpNormal = 0
    'Include the data sections from all loaded modules. This results in the
    'inclusion of global variables, which can make the minidump file significantly
    'larger. For per-module control, use the ModuleWriteDataSeg enumeration
    'value from MODULE_WRITE_FLAGS.
    MiniDumpWithDataSegs = 1
    'Include all accessible memory in the process. The raw memory data is included
    'at the end, so that the initial structures can be mapped directly without the
    'raw memory information. This option can result in a very large file.
    MiniDumpWithFullMemory = 2
    'Stack and backing store memory written to the minidump file should be filtered
    'to remove all but the pointer values necessary to reconstruct a stack trace.
    'Typically, this removes any private information.
    MiniDumpWithHandleData = 4
    'Include high-level information about the operating system handles that
    'are active when the minidump is made.
    MiniDumpFilterMemory = 8
    'Stack and backing store memory should be scanned for pointer references
    'to modules in the module list. If a module is referenced by stack or backing
    'store memory, the ModuleWriteFlags member of the MINIDUMP_CALLBACK_OUTPUT
    'structure is set to ModuleReferencedByMemory.
    MiniDumpScanMemory = 10

    'the following aren't supported under XP
    'Include information from the list of modules that were recently unloaded,
    'if this information is maintained by the operating system.
    MiniDumpWithUnloadedModules = 20
    'Include pages with data referenced by locals or other stack memory. This
    'option can increase the size of the minidump file significantly.
    MiniDumpWithIndirectlyReferencedMemory = 40
    'Filter module paths for information such as user names or important
    'directories. This option may prevent the system from locating the image
    'file and should be used only in special situations.
    MiniDumpFilterModulePaths = 80
    'Include complete per-process and per-thread information from the operating
    'system.
    MiniDumpWithProcessThreadData = 100
    'Scan the virtual address space for other types of memory to be included.
    MiniDumpWithPrivateReadWriteMemory = 200
    'Reduce the data that is dumped by eliminating memory regions that are not
    'essential to meet criteria specified for the dump. This can avoid dumping
    'memory that may contain data that is private to the user. However, it is not
    'a guarantee that no private information will be present.
    MiniDumpWithoutOptionalData = 400
    'Include memory region information. For more information,
    'see MINIDUMP_MEMORY_INFO_LIST.
    MiniDumpWithFullMemoryInfo = 800
    'Include thread state information. For more information,
    'see MINIDUMP_THREAD_INFO_LIST.
    MiniDumpWithThreadInfo = 1000
    'Include all code and code-related sections from loaded modules to
    'capture executable content. For per-module control, use the ModuleWriteCodeSegs
    'enumeration value from MODULE_WRITE_FLAGS.
    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:

VB.NET
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)

            'Call the API
            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):

VB.NET
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:

VB.NET
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:

VB.NET
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.

VB.NET
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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)