Results 1 to 5 of 5

Thread: Global error handler with stack tracing

  1. #1
    Software Carpenter dee-u's Avatar
    Join Date
    Feb 05
    Location
    Philippines
    Posts
    10,221

    Arrow Global error handler with stack tracing

    This is just a sample of how to use a global error handler that will log application errors together with a stack tracing implementation.

    These are the main functions that does the logging.
    VB6 Code:
    1. Public Function PromptAndLogError(ByVal strErrorSource As String, Optional ByVal strOtherInfo As String = vbNullString, Optional ByVal ReplyButton As Long = 2) As Long
    2.     Dim strErrorMessage As String
    3.     Dim strMsg          As String
    4.     Dim strStack        As String
    5.     Dim strErrDesc      As String
    6.     Dim strErrNum       As String
    7.     Dim strErl          As String
    8.     Dim dteErrorOccured As Date
    9.        
    10.     'store the pertinent error values
    11.     strErrDesc = Err.Description
    12.     strErrNum = Err.Number
    13.     strErl = Erl
    14.    
    15.     dteErrorOccured = Now 'get date
    16.     strStack = GetTraces  'get stack trace
    17.    
    18.     strErrorSource = ProgramName & "." & strErrorSource
    19.    
    20.     If Len(strStack) > 0 Then
    21.         strErrorMessage = "Last Trace:        " & strStack & vbCrLf
    22.     End If
    23.     'this will be the logged message, it will contain the stack trace
    24.     strErrorMessage = strErrorMessage & _
    25.                     "Error Occured In:  " & strErrorSource & vbCrLf & _
    26.                     "Error Date & Time: " & dteErrorOccured & vbCrLf & _
    27.                     "Error Description: " & strErrDesc & vbCrLf & _
    28.                     "Error Number:      " & strErrNum & vbCrLf & _
    29.                     "Line Number:       " & strErl
    30.  
    31.     'save other pertinent data that you want to be logged
    32.     If Len(strOtherInfo) > 0 Then
    33.         strErrorMessage = strErrorMessage & (vbCrLf & strOtherInfo)
    34.     End If
    35.    
    36.     'this will be the message to be show to the user
    37.     strMsg = "Error Occured In: " & strErrorSource & vbCrLf & _
    38.              "Error Date & Time: " & dteErrorOccured & vbCrLf & _
    39.              "Error Description: " & strErrDesc & vbCrLf & _
    40.              "Error Number: " & strErrNum & vbCrLf & _
    41.              "Line Number: " & strErl
    42.    
    43.     PromptAndLogError = LogAndProcessError(strMsg, strErrorMessage, dteErrorOccured, True, ReplyButton)
    44. End Function
    45.  
    46. Private Function LogAndProcessError(ByVal MsgBoxError As String, ByVal ErrorMessage As String, ByVal dteErrorOccured As Date, Optional ShowPrompt As Boolean = True, Optional ByVal ReplyButton As Long = 2) As Long
    47.         On Error GoTo HandleError
    48.        
    49.         Dim FileNum         As Integer
    50.         Dim strErrorMessage As String
    51.         Dim Response        As String
    52.         Dim ErrorFile       As String
    53.         Dim PCName          As String
    54.         Dim ScreenFile      As String
    55.         Dim strData         As String
    56.         Dim ProgramPath     As String
    57.        
    58. 100     SkipTrace = True
    59. 102     ProgramPath = strAppData
    60.        
    61.         'also log error in event viewer
    62. 104     App.StartLogging "", vbLogToNT
    63. 106     App.LogEvent ErrorMessage, vbLogEventTypeError
    64.    
    65.         'get computer name
    66. 108     PCName = GetPCName
    67.  
    68.         'create directory where the error will be logged if the directory does not yet exist
    69. 110     If DirectoryExists(ProgramPath & "\ErrorLog\") = False Then
    70. 112         CreateFolder ProgramPath & "\ErrorLog\"
    71.         End If
    72.        
    73.         'this will create a unique file for each day
    74. 114     ErrorFile = ProgramPath & "\ErrorLog\ErrorLog-" & PCName & "-" & Format$(dteErrorOccured, "mmddyyyy") & ".txt"
    75. 116     ScreenFile = ProgramPath & "\ErrorLog\ErrorLog-" & PCName & "-" & Format$(dteErrorOccured, "mmddyyyy-hhnnss") & ".jpg"
    76.        
    77. 118     If ShowPrompt = True Then
    78. 120         LogAndProcessError = MsgBox(MsgBoxError & vbCrLf & vbCrLf & "Please report to the programmer any persistent error.   ", vbCritical + ReplyButton + vbDefaultButton2, "Unexpected Error Encountered")
    79.             'log also the response of the user
    80. 122         Select Case LogAndProcessError
    81.             Case vbAbort
    82. 124             Response = "vbAbort"
    83. 126         Case vbRetry
    84. 128             Response = "vbRetry"
    85. 130         Case vbIgnore
    86. 132             Response = "vbIgnore"
    87. 134         Case vbOK
    88. 136             Response = "vbOK"
    89.             End Select
    90. 138         If Len(Response) > 0 Then
    91. 140             ErrorMessage = ErrorMessage & vbNewLine & "Response:          " & Response
    92.             End If
    93.         End If
    94.    
    95.         'if the log file already exist then append the new error log
    96. 142     If FileExists(ErrorFile) = True Then
    97. 144         strData = vbCrLf & String$(20, "-") & vbCrLf & ErrorMessage
    98.         Else
    99. 146         strData = ErrorMessage
    100.         End If
    101.    
    102. 148     FileNum = FreeFile
    103. 150     Open ErrorFile For Append As FileNum
    104. 152     Print #FileNum, strData
    105. 154     Close FileNum
    106.    
    107.         'capture screenshot
    108. 156     Capture_Desktop ScreenFile
    109.        
    110. 158     Busy False
    111. 160     SkipTrace = False
    112.  
    113.         Exit Function
    114. HandleError:
    115.     strErrorMessage = vbNewLine & _
    116.              "Error Occured In:  " & "LogAndProcessError" & vbCrLf & _
    117.              "Error Date & Time: " & dteErrorOccured & vbCrLf & _
    118.              "Error Description: " & Err.Description & vbCrLf & _
    119.              "Error Number:      " & Err.Number & vbCrLf & _
    120.              "Line Number:       " & Erl
    121.     LogMessage strErrorMessage
    122.     Busy False
    123. End Function

    And for the stack tracing:
    VB6 Code:
    1. Public Sub PushStack(ByVal strProcedure As String)
    2.     'when in error handling routine then don't register traces
    3.     If SkipTrace = True Then Exit Sub
    4.  
    5.     If ArrayInit(Not colStacks) = False Then
    6.         'initialized
    7.         ReDim Preserve colStacks(0)
    8.         colStacks(0) = strProcedure
    9.     Else
    10.         'increase size
    11.         ReDim Preserve colStacks(UBound(colStacks) + 1)
    12.         colStacks(UBound(colStacks)) = strProcedure
    13.     End If
    14. End Sub
    15.  
    16. Public Sub PopStack()
    17.     If SkipTrace = True Then Exit Sub
    18.  
    19.     If ArrayInit(Not colStacks) = True Then
    20.         If UBound(colStacks) = LBound(colStacks) Then
    21.             Erase colStacks
    22.         Else
    23.             ReDim Preserve colStacks(UBound(colStacks) - 1)
    24.         End If
    25.     End If
    26. End Sub

    Sample usage:
    VB6 Code:
    1. Private Sub Command1_Click()
    2.         PushStack "Form1.Command1_Click"
    3.         On Error GoTo HandleError
    4.    
    5.         Dim a As Long
    6.         Dim b As Long
    7.         Dim c As Long
    8.        
    9. 100     a = 0
    10. 102     b = 1
    11. 104     c = b / a 'division by zero
    12.    
    13. ExitProcedure:
    14.         PopStack
    15.         Exit Sub
    16. HandleError:
    17.         Select Case PromptAndLogError("Form1.Command1_Click")
    18.         Case vbAbort
    19.              GoTo ExitProcedure
    20.         Case vbRetry
    21.              Resume
    22.         Case vbIgnore
    23.              Resume Next
    24.         Case Else
    25.              SignalCriticalError Err.Description
    26.              End
    27.         End Select
    28. End Sub

    If you happen to use MZTools then this will be the template that you can use:
    Code:
    PushStack "{MODULE_NAME}.{PROCEDURE_NAME}"
       On Error GoTo HandleError
    
    	{PROCEDURE_BODY}
    
    ExitProcedure:
       PopStack
       Exit {PROCEDURE_TYPE}
    
    HandleError:
    
    	Select Case PromptAndLogError("{MODULE_NAME}.{PROCEDURE_NAME}")
            Case vbAbort
                 GoTo ExitProcedure
            Case vbRetry
                 Resume
            Case vbIgnore
                 Resume Next
            Case Else
                 SignalCriticalError Err.Description
                 End
            End Select
    Have a look at the attached project for a complete demo.
    Attached Files Attached Files
    Last edited by dee-u; Aug 31st, 2009 at 03:47 AM. Reason: added MZTools template

  2. #2
    PowerPoster Ellis Dee's Avatar
    Join Date
    Mar 07
    Location
    New England
    Posts
    3,438

    Re: Global error handler with stack tracing

    Mine is the same basic idea, also using a call stack. I kept it all as a single function in order to make interacting with it easier.

    I attached a rudimentary sample "project" to demonstrate how it works. It's just a bas file, but if you double-click it to open in VB6 and then click Run it should work fine.
    vb Code:
    1. Public Const ErrorIgnore = vbObjectError + 513
    2. Public Const ErrorNotice = vbObjectError + 514
    3. Public Const ErrorData = vbObjectError + 515
    4. Public Const ErrorFail = vbObjectError + 516
    5.  
    6. Public Enum ErrorHandlerActions
    7.     eaEnter
    8.     eaExit
    9.     eaLog
    10.     eaNotify
    11.     eaRaise
    12.     eaRoot
    13. End Enum
    14.  
    15. ' ErrorHandler()
    16. ' This function is designed to offer robust and flexible
    17. ' error handling with a minimum of impact to the bulk of
    18. ' your code. It allows you to handle errors in data
    19. ' modules, classes, or generic utilities seemlessly.
    20. '
    21. ' Concept
    22. ' Use "On Error" trapping only in root-level functions
    23. ' (defined as any event procedure fired by a user
    24. ' interaction or timer control) and functions that require
    25. ' a close-down section, such as those that access a
    26. ' database or create objects. No other error trapping
    27. ' is required. (The root-level trapping will catch any
    28. ' subsequent errors down the call stack, so additional
    29. ' trapping would be redundant.)
    30. '
    31. ' Requirements
    32. ' ErrorHandler() requires a public enumeration as follows:
    33. 'Public Enum ErrorHandlerActions
    34. '    eaEnter
    35. '    eaExit
    36. '    eaLog
    37. '    eaNotify
    38. '    eaRaise
    39. '    eaRoot
    40. 'End Enum
    41. '
    42. ' Call Stack
    43. ' ErrorHandler() tracks the call stack as provided by you
    44. ' the programmer. Send the function/sub/property/method
    45. ' procedure name to ErrorHandler() as the first line of
    46. ' code in each procedure. You should include the module
    47. ' name as well; one simple idea is to define a private
    48. ' constant in all modules that contains the module name,
    49. ' and then add the function name in each individual call.
    50. '
    51. ' Root-Level functions
    52. ' For root-level functions (which will have error trapping)
    53. ' add to the call stack using the eaRoot action. These are
    54. ' also the functions where user notification is handled by
    55. ' calling the eaNotify action in the error trapping
    56. ' routine. Note that the eaExit action (discussed later)
    57. ' isn't needed in root-level functions. For example:
    58. 'Public Sub List1_Click()
    59. 'On Error GoTo List1_ClickErr
    60. '    ErrorHandler eaRoot, ModuleConstant & ".List1_Click"
    61. '    ' Your code to process the event goes here
    62. '
    63. 'List1_ClickExit:
    64. '    Exit Sub
    65. '
    66. 'List1_ClickErr:
    67. '    ErrorHandler eaNotify
    68. '    Resume List1_ClickExit
    69. 'End Sub
    70. '
    71. ' For "standard" functions, defined as being neither root-
    72. ' level nor requiring a close-down section, manage the
    73. ' call stack by beginning and ending each function with
    74. ' the eaEnter and eaExit actions respectively. Like the
    75. ' eaRoot action, eaEnter requires the function name. Also
    76. ' remember that error trapping is unnecessary in standard
    77. ' functions. Example:
    78. 'Public Sub Refresh()
    79. '    ErrorHandler eaEnter, ModuleConstant & ".Refresh"
    80. '    ' Your code goes here
    81. '    ErrorHandler eaExit
    82. 'End Sub
    83. '
    84. ' In non-root-level functions that require a close-down
    85. ' section, include eaEnter and eaExit actions just like
    86. ' with standard functions, but there is additional support
    87. ' required. Your error trapping that forces a close-down
    88. ' section (releasing objects or closing recordsets) to fire
    89. ' will absorb the actual error, leaving nothing to pass
    90. ' back up the call stack for ErrorHandler() to use in
    91. ' notifying the user. This problem is made worse because
    92. ' errors are typically suppressed (via On Error Resume
    93. ' Next) during close-down sections.
    94. '
    95. ' ErrorHandler() provides a simple solution for this
    96. ' dilemma. Send the eaLog action in the error trapping
    97. ' routine to log the error for later use. In your close-
    98. ' down code, after it is finished be sure error trapping
    99. ' is re-enabled (with On Error Goto 0) and send the
    100. ' eaRaise action to propagate the error back up the call
    101. ' stack. Here is a complete example:
    102. 'Public Function GetEmployeeName() As String
    103. 'On Error GoTo GetEmployeeNameErr
    104. '    ErrorHandler eaEnter, ModuleConstant & ".GetEmployeeName"
    105. '    ' Your code to access data/create objects goes here
    106. '    ErrorHandler eaExit
    107. '
    108. 'GetEmployeeNameExit:
    109. '    On Error Resume Next
    110. '    ' Your code to close recordsets/objects goes here
    111. '    On Error GoTo 0
    112. '    ErrorHandler eaRaise
    113. '    Exit Function
    114. '
    115. 'GetEmployeeNameErr:
    116. '    ErrorHandler eaLog
    117. '    Resume GetEmployeeNameExit
    118. 'End Function
    119. '
    120. ' Custom errors
    121. ' Your program will no doubt either have custom errors, or
    122. ' at least specific custom messages to some default errors.
    123. ' Modify the Select Case statement to handle your specific
    124. ' errors as appropriate.
    125. '
    126. ' Action Summary
    127. ' eaEnter
    128. '     Requires a source name. (Class/Module.Function)
    129. '     Used at start of all non-root-level functions.
    130. ' eaExit
    131. '     No source name required.
    132. '     Used at end of all non-root-level functions.
    133. ' eaLog
    134. '     No source name required.
    135. '     Used in error trapping routine when a close-down
    136. '     section is required. (Function uses data or objects.)
    137. ' eaNotify
    138. '     No source name required.
    139. '     Used in error trapping routine of root-level functions.
    140. ' eaRaise
    141. '     No source name required.
    142. '     Used at end of close-down sections.
    143. ' eaRoot
    144. '     Requires a source name. (Class/Module.Function)
    145. '     Used at start of all root-level functions.
    146. Public Sub ErrorHandler(penAction As ErrorHandlerActions, Optional pstrSource As String)
    147.     Const Delimiter = vbCrLf & "    "
    148.     Static slngNumber As Long
    149.     Static sstrSource As String
    150.     Static sstrDescription As String
    151.     Static sstrCallStack As String
    152.     Dim strMessage As String
    153.     Dim strDetail As String
    154.    
    155.     ' Clear error state if entering a root function
    156.     If penAction = eaRoot Then
    157.         Err.Clear
    158.         slngNumber = 0
    159.         sstrSource = ""
    160.         sstrDescription = ""
    161.         sstrCallStack = ""
    162.     End If
    163.     ' Remove source from list if successfully exiting a function
    164.     If penAction = eaExit Then
    165.         If InStr(sstrCallStack, Delimiter) > 0 Then
    166.             sstrCallStack = Left(sstrCallStack, InStrRev(sstrCallStack, Delimiter, Len(sstrCallStack) - 1) - 1)
    167.         End If
    168.         Exit Sub
    169.     End If
    170.     ' Log source
    171.     If penAction = eaRoot Or penAction = eaEnter Then
    172.         sstrCallStack = sstrCallStack & Delimiter & pstrSource
    173.     End If
    174.     ' Log error number and description if in an error state
    175.     If Err.Number <> 0 Then ' If penAction = eaLog Or penAction = eaNotify Then
    176.         slngNumber = Err.Number
    177.         sstrSource = Err.Source
    178.         sstrDescription = Err.Description
    179.     End If
    180.     ' Raise error
    181.     If penAction = eaRaise And slngNumber <> 0 Then
    182.         Err.Raise slngNumber, sstrSource, sstrDescription
    183.     End If
    184.     ' Notify user
    185.     If penAction = eaNotify And slngNumber <> 0 Then
    186.         ' Update the screen
    187.         Screen.MousePointer = vbNormal
    188.         'UnlockWindow
    189.         strDetail = _
    190.             "Number: " & slngNumber & " (" & slngNumber - vbObjectError & ")" & vbCrLf & _
    191.             "Source: " & sstrSource & vbCrLf & _
    192.             "Description: " & sstrDescription & vbCrLf & vbCrLf & _
    193.             "Call stack:" & sstrCallStack
    194.         ' Show message
    195.         Select Case slngNumber
    196.             Case ErrorIgnore
    197.             Case ErrorNotice
    198.                 MsgBox sstrDescription, vbInformation, "Notice"
    199.             Case Else ' Unexpected error
    200.                 strMessage = sstrDescription & vbCrLf & vbCrLf & "View detail?"
    201.                 If MsgBox(strMessage, vbYesNo + vbInformation + vbDefaultButton2, "Notice") = vbYes Then
    202.                     Clipboard.Clear
    203.                     Clipboard.SetText strMessage, vbCFText
    204.                     strDetail = strDetail & vbCrLf & vbCrLf & "Detail has been copied to the clipboard."
    205.                     MsgBox strDetail, vbExclamation, "Error Detail"
    206.                 End If
    207.         End Select
    208.     End If
    209.     ' Clear error state
    210.     If penAction = eaNotify Then
    211.         Err.Clear
    212.         slngNumber = 0
    213.         sstrCallStack = ""
    214.         sstrDescription = ""
    215.         sstrSource = ""
    216.     End If
    217. End Sub
    Attached Files Attached Files
    Last edited by Ellis Dee; Sep 16th, 2009 at 04:42 AM.

  3. #3
    Software Carpenter dee-u's Avatar
    Join Date
    Feb 05
    Location
    Philippines
    Posts
    10,221

    Re: Global error handler with stack tracing

    Hi Ellis Dee, your root-level function has got my attention. I have read this tip and considering modifying my implementation to the recommendation given, I want to ask if do you consider an "event handler" a root-level function?

  4. #4
    PowerPoster Ellis Dee's Avatar
    Join Date
    Mar 07
    Location
    New England
    Posts
    3,438

    Re: Global error handler with stack tracing

    Yes, that's exactly what a root level function is. Looks like I stumbled upon the same idea as the linked article recommends, though I see no point in adding error handling outside of root level functions other than functions where you have to close objects or database connections.

    Had I been more thoughtful, "event handlers" would have been a much more descriptive name instead of "root level functions." They are the same thing. The easiest way to think of them is that they're always the root entry in a call stack. (Thus the name I picked.)

  5. #5
    PowerPoster Ellis Dee's Avatar
    Join Date
    Mar 07
    Location
    New England
    Posts
    3,438

    Re: Global error handler with stack tracing

    From the linked article:
    Imagine you have opened a file and are attempting to roll back in your error handler. How do you know whether or not you opened the file? In other words, did the error occur before or after the line of code that opens the file? If you attempt to close the file and it's not open, you'll cause an error-but if it's open, you don't want to leave it open as you're trying to roll back! I guess you could use Erl to determine where your code erred, but this implies that you're editing line numbered source code-yuck. (You'll recall from Tip 2 that we added line numbers only to the code for the final EXE, not to the code we're still editing.) Probably the best way to determine what did or did not get done is to limit the possibilities; that is, keep your routines small (so that you have only a small problem domain). Of course, that's not going to help us here. What we need to do is apply a little investigation!
    I don't seem to have these issues with my implementation. This type of function would be what I call a non-root function that needs to close objects. So it would go like this:
    Code:
    Public Function ReadFile() As String
    On Error GoTo ReadFileErr
        Dim lngFileNumber As Long
    
        ErrorHandler eaEnter, ModuleConstant & ".ReadFile"
        ' Your code to access data/create objects goes here
        lngFileNumber = FreeFile()
        ' Open file and do stuff here
        ErrorHandler eaExit
    
    ReadFileExit:
        On Error Resume Next
        Close lngFileNumber
        On Error GoTo 0
        ErrorHandler eaRaise
        Exit Function
    
    ReadFileErr:
        ErrorHandler eaLog
        Resume ReadFileExit
    End Function
    Also from the linked article:
    If you've cleaned up locally all the way through the call chain and if you had an error handler for each stack frame (so that you didn't jump over some routines), you should now have effectively rolled back from the error.
    I see no point in trapping errors down the call stack that have no cleanup requirements. In other words, why would it be a problem to jump over routines that didn't need to close files or release objects? I don't get it.

    Every function has the Enter and Exit calls in order to track the call stack, but why would actual trapping be required?
    Last edited by Ellis Dee; Sep 21st, 2009 at 02:53 PM.

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •