Thursday, September 10, 2020

Advanced Error Handling in MS Access

This VBA snippet will execute the following actions:

  • Save error details in tblErrorLog table
  • It will add the current method if it does not exist in tblVBAMethods table
  • Allow user to report the error which by sending an email with screen dump taken and attached automatically

The script is referencing the following tables:

      CREATE TABLE tblErrorLog(

            ErrorID LONG AUTOINCREMENT,

            MethodID TEXT(255) NOT NULL,

            LineNo LONG NOT NULL,

            ErrorNo LONG NOT NULL,

            ErrorDescription MEMO NOT NULL,

            DateCreated DATETIME NOT NULL,

            CreatedBy TEXT(255) NOT NULL,

            DateModified DATETIME,

            ModifiedBy TEXT(255)

);

CREATE TABLE tblVBAMethods(

    MethodID TEXT(255) NOT NULL,

    Component TEXT(100) NOT NULL,

    Method TEXT(100) NOT NULL,

    DateCreated DATETIME NOT NULL,

    CreatedBy TEXT(255) NOT NULL,

    DateModified DATETIME,

    ModifiedBy TEXT(255)

);


CREATE TABLE tblVBAComponents(

    Component TEXT(100) NOT NULL,

    IsSelected BOOL,

    DateCreated DATETIME NOT NULL,

    CreatedBy TEXT(255) NOT NULL,

    DateModified DATETIME,

    ModifiedBy TEXT(255)

);


CREATE TABLE tblAppSettings(

    ConstID TEXT(255) NOT NULL,

    ConstDesc TEXT(255) NOT NULL,

    ConstValue TEXT(255) NOT NULL,

    DateCreated DATETIME NOT NULL,

    CreatedBy TEXT(255) NOT NULL,

    DateModified DATETIME,

    ModifiedBy TEXT(255)

);


Option Compare Database

Option Explicit

Option Base 1

Function TestErrorMessage()

On Error GoTo ErrorExit

    Dim RaiseVal As Integer

10      RaiseVal = 1 / 0

NormalExit:

    Exit Function

ErrorExit:

    ErrNumber = Err.Number: ErrDescription = Err.Description: LineNo = Erl

    MethodID = VBE.ActiveCodePane.CodeModule.Name + "." + GetFnOrSubName(Erl)

    WhereIs = "Raised on line " + IIf(CStr(LineNo) = "0", "Unknown", CStr(LineNo)) + " of " + MethodID + " method."

    ErrorPrompt = "Error " + CStr(ErrNumber) + " - " + ErrDescription

    ErrorID = LogError(MethodID, LineNo, ErrNumber, ErrDescription)

    Answer = MsgBox(WhereIs + ". " + vbCr + vbCr + "Do you want to report it?", (vbCritical + vbQuestion + vbYesNo + vbDefaultButton2), ErrorPrompt)

    If Answer = vbYes Then Call ReportError(ErrorID): Resume NormalExit

End Function

Function LogError(MethodID As String, LineNo As Variant, ErrNumber As Integer, ErrDescription As String) As Long

On Error GoTo ErrorExit

    Dim strMsg As String, strSql As String, ErrorID As Long, StartDateTime As Date, strWhere As String, MethodCounter As Integer _

        , Scope As String, Component As String, Method As String

        StartDateTime = Now()

        LogError = 0

        Call InitApp

        MethodCounter = DCount("*", "tblVBAMethods", "MethodID = '" + MethodID + "'")

        If MethodCounter = 0 Then

            Scope = VBE.ActiveCodePane.CodeModule.Name

            Component = VBE.SelectedVBComponent.Name

            Method = Mid(MethodID, InStr(1, MethodID, ".") + 1, Len(MethodID))

            Call AddMethod(MethodID, Scope, Component, Method, SeekerEmail, StartDateTime)

        End If

        strSql = "INSERT INTO tblErrorLog ( MethodID, LineNo, ErrorNo, ErrorDeScription, DateCreated, CreatedBy)"

        strSql = strSql + " VALUES('" + MethodID + "'"

        strSql = strSql + ", '" + CStr(LineNo) + "'"

        strSql = strSql + ", '" + CStr(ErrNumber) + "'"

        strSql = strSql + ", '" + ErrDescription + "'"

        strSql = strSql + ", '" + CStr(StartDateTime) + "'"

        strSql = strSql + ", '" + SeekerEmail + "')"

        DoCmd.SetWarnings False

        DoCmd.RunSQL strSql

        DoCmd.SetWarnings True

        strWhere = "CStr(DateCreated) = '" + CStr(StartDateTime) + "' and CreatedBy = '" + SeekerEmail + "'"

        LogError = DLookup("ErrorID", "tblErrorLog", strWhere)

NormalExit:

    Exit Function

ErrorExit:

    ErrNumber = Err.Number: ErrDescription = Err.Description: LineNo = Erl

    MethodID = VBE.ActiveCodePane.CodeModule.Name + "." + GetFnOrSubName(Erl)

    WhereIs = "Raised on line " + IIf(CStr(LineNo) = "0", "Unknown", CStr(LineNo)) + " of " + MethodID + " method."

    ErrorPrompt = "Error " + CStr(ErrNumber) + " - " + ErrDescription

    ErrorID = LogError(MethodID, LineNo, ErrNumber, ErrDescription)

    Answer = MsgBox(WhereIs + ". " + vbCr + vbCr + "Do you want to report it?", (vbCritical + vbQuestion + vbYesNo + vbDefaultButton2), ErrorPrompt)

    If Answer = vbYes Then Call ReportError(ErrorID): Resume NormalExit

End Function

Function ReportError(ErrorID As Long) As Boolean

On Error GoTo ErrorExit

    Dim Style As Variant, Title As Variant, SupportEmail As String, RetVal As String, TestVal As Boolean, frmMsg As String _

        , PowerShellFile As String, BMPfile As String, waitOnReturn As Boolean, windowStyle As Integer

        ReportError = False

        windowStyle = 1: waitOnReturn = True

        Call PrintScreen

        SupportEmail = Nz(DLookup("ConstValue", "tblAppSettings", "ConstID='SupportEmail'"), vbNullString)

        PowerShellFile = Application.CurrentProject.path + "\ErrorMessages\" + SeekerEmail + "\" + "SpoolClipboardImageToBMPfile.ps1"

        BMPfile = Application.CurrentProject.path + "\ErrorMessages\" + SeekerEmail + "\"

        BMPfile = BMPfile + "Error_" + Format$(Now, "yyyy_mm_dd_hh_mm_ss") + ".bmp"

        TestVal = SpoolClipboardImageToBMPfile(PowerShellFile, BMPfile)

        If Not TestVal Then GoTo NormalExit

        TestVal = SendEmail(EmailTo:=SupportEmail _

            , EmailSubject:=ErrorPrompt + WhereIs _

            , EmailBody:="ErrorID is " + CStr(ErrorID) _

            , Attachment:=BMPfile)

        Call Pause(2)

        If Not TestVal Then GoTo NormalExit

        TestVal = LetsDeleteFile(BMPfile)

        If Not TestVal Then GoTo NormalExit

        ReportError = True

NormalExit:

    Exit Function

ErrorExit:

    ErrNumber = Err.Number: ErrDescription = Err.Description: LineNo = Erl

    MethodID = VBE.ActiveCodePane.CodeModule.Name + "." + GetFnOrSubName(Erl)

    WhereIs = "Raised on line " + IIf(CStr(LineNo) = "0", "Unknown", CStr(LineNo)) + " of " + MethodID + " method."

    ErrorPrompt = "Error " + CStr(ErrNumber) + " - " + ErrDescription

    ErrorID = LogError(MethodID, LineNo, ErrNumber, ErrDescription)

    Answer = MsgBox(WhereIs + ". " + vbCr + vbCr + "Do you want to report it?", (vbCritical + vbQuestion + vbYesNo + vbDefaultButton2), ErrorPrompt)

    If Answer = vbYes Then Call ReportError(ErrorID): Resume NormalExit

End Function

Function GetFnOrSubName(handlerLabel As String) As String

On Error GoTo ErrorExit

    Dim VBProj As VBIDE.VBProject, vbComp As VBIDE.VBComponent, CodeMod As VBIDE.CodeModule, code$, handlerAt&, isFunction&, isSub&

        Set VBProj = VBE.ActiveVBProject

        Set vbComp = VBProj.VBComponents(VBE.ActiveCodePane.CodeModule.Name)

        Set CodeMod = vbComp.CodeModule

        code = CodeMod.Lines(1, CodeMod.CountOfLines)

        handlerAt = InStr(1, code, handlerLabel, vbTextCompare)

        If handlerAt Then

            isFunction = InStrRev(Mid$(code, 1, handlerAt), "Function", -1, vbTextCompare)

            isSub = InStrRev(Mid$(code, 1, handlerAt), "Sub", -1, vbTextCompare)

            If isFunction > isSub Then

                'It's a function

                GetFnOrSubName = Replace(Split(Mid$(code, isFunction, 40), "(")(0), "Function ", vbNullString)

            Else

                'It's a sub

                GetFnOrSubName = Replace(Split(Mid$(code, isSub, 40), "(")(0), "Sub ", vbNullString)

            End If

        End If

NormalExit:

    Exit Function

ErrorExit:

    ErrNumber = Err.Number: ErrDescription = Err.Description: LineNo = Erl

    MethodID = VBE.ActiveCodePane.CodeModule.Name + "." + GetFnOrSubName(Erl)

    WhereIs = "Raised on line " + IIf(CStr(LineNo) = "0", "Unknown", CStr(LineNo)) + " of " + MethodID + " method."

    ErrorPrompt = "Error " + CStr(ErrNumber) + " - " + ErrDescription

    ErrorID = LogError(MethodID, LineNo, ErrNumber, ErrDescription)

    Answer = MsgBox(WhereIs + ". " + vbCr + vbCr + "Do you want to report it?", (vbCritical + vbQuestion + vbYesNo + vbDefaultButton2), ErrorPrompt)

    If Answer = vbYes Then Call ReportError(ErrorID): Resume NormalExit

End Function

Sub PrintScreen()

    keybd_event VK_SNAPSHOT, 1, 0, 0

End Sub

Function SpoolClipboardImageToBMPfile(PowerShellFile As String, BMPfile As String) As Boolean

On Error GoTo ErrorExit

    Dim objWScript As Object, psCmd As String, TestVal As Integer, MkDirFavorites As String, WshExec As Object _

        , waitOnReturn As Boolean, windowStyle As Integer, ErrorCode As Integer

        SpoolClipboardImageToBMPfile = False

        Set objWScript = CreateObject("WScript.Shell")

         psCmd = "get-clipboard -format image" + vbCr

        psCmd = psCmd + "$img = get-clipboard -format image" + vbCr

        psCmd = psCmd + "$img.save('" + BMPfile + "')" + vbCr

        TestVal = LetsDeleteFile(PowerShellFile)

        If Not TestVal Then GoTo NormalExit

        TestVal = LetsCreateFile(PowerShellFile, psCmd)

        If Not TestVal Then GoTo NormalExit

        psCmd = "Powershell -File """ + PowerShellFile + """"

        ErrorCode = objWScript.Run(psCmd, vbHide, waitOnReturn)

        If ErrorCode > 0 Then GoTo NormalExit

        SpoolClipboardImageToBMPfile = True

NormalExit:

    Set objWScript = Nothing

    Exit Function

ErrorExit:

    ErrNumber = Err.Number: ErrDescription = Err.Description: LineNo = Erl

    MethodID = VBE.ActiveCodePane.CodeModule.Name + "." + GetFnOrSubName(Erl)

    WhereIs = "Raised on line " + IIf(CStr(LineNo) = "0", "Unknown", CStr(LineNo)) + " of " + MethodID + " method."

    ErrorPrompt = "Error " + CStr(ErrNumber) + " - " + ErrDescription

    ErrorID = LogError(MethodID, LineNo, ErrNumber, ErrDescription)

    Answer = MsgBox(WhereIs + ". " + vbCr + vbCr + "Do you want to report it?", (vbCritical + vbQuestion + vbYesNo + vbDefaultButton2), ErrorPrompt)

    If Answer = vbYes Then Call ReportError(ErrorID): Resume NormalExit

End Function

If you find this script useful please donate generously.