Error 1004 in Access VBA - excel

I have spent a few hours researching this Error 1004 problem here, but did not find a similar situation.
In an access module I have 3 global objects (global in the module only) as follows:
Private objXLapp As Excel.Application
Private objXLworkbook As Excel.Workbook
Private objXLworksheet As Excel.Worksheet
All are instantiated correctly.
I am transferring data from a table into a spreadsheet through a function that is repeatedly called from within the module. The bare bones of the routines are below.
Private Function CreateLevel2(ByRef rstCASHFLOW As DAO.Recordset, ByRef currentRow As Long, colRef As String) As Boolean
Const PROC_NAME = "CreateLevel2"
On Local Error GoTo CatchAllErrors
Do
Retry:
objXLworksheet.Range("C" & currentRow - 1).Formula = rstCASHFLOW!PMTlevel2Code
... other code here
rstCASHFLOW.MoveNext
Loop Until rstCASHFLOW.EOF
Exit_Func:
On Local Error GoTo 0
Exit Function
CatchAllErrors:
If Err.Number = 1004 then
MsgBox "Press ok to continue"
Goto Retry
End If
Set ErrorMessage.OrgError = err
NewMsgBox eemUnknownError, MODULE_NAME, PROC_NAME
Err.Clear
Goto Exit_Func
End Function
Private Function CreateLevel1() As Boolean
Const PROC_NAME = "CreateLevel1"
On Local Error GoTo CatchAllErrors
objXLworksheet.Range("B" & currentRow - 2).Formula = rstCASHFLOW!PMTlevel1Code
… other code here
Exit_Func:
On Local Error GoTo 0
Exit Function
CatchAllErrors:
Set ErrorMessage.OrgError = Err
NewMsgBox eemUnknownError, MODULE_NAME, PROC_NAME
Err.Clear
GoTo Exit_Func
End Function
If I remove the "On Error" the program stops at the line of code assigning a value to the worksheet in the Function "CreateLevel2". If I exam all variables, none have lost scope and are valid. If I then tell the program to resume, it continues with no problem and the correct data is transferred to the spreadsheet.
If I include the "On Error" to catch the error and pop up a msgbox asking me to proceed (effectively suspending execution until I press ok), the code continues without any problems.
Is this just some querk of Access VBA?
When the error occurs, the variable currentRow is 13, the table variable rstCASHFLOW!PMTlevel2Code has a value (a string) and rstCASHFLOW.EOF is false. The XL objects are also ok. Checking through the Watch Window shows me that the object references are all fine.
Note there is only one objXLapp, one objXLworkbook and one objXLworksheet. The workbook only contains 1 worksheet.
I am totally stumped!
Can anyone help... thanks

Related

GetObject("winmgmts:... crashes Excel 2016 with no Errors

I am debugging some VBA code I've written in Excel 2016, and this sub is crashing Excel 2016 on windows Server with no errors.
It is crashing on the Set RegObj = GetObject...
Sub TestPrinter()
On Error GoTo e
Dim RegObj As Object
'This next line is where the crash occurs...
Set RegObj = GetObject("winmgmts:{impersonationLevel=impersonate}!\\.\root\default:StdRegProv")
Exit Sub
e:
MsgBox "Error number " & Err & " in TestPrinter" & vbCrLf & "Error: " & Error$(Err)
End Sub
My end goal is to enumerate the printers connected on the machine, and then set Application.ActivePrinter based on the string I pull out of the registry. This code is working fine on every other machine I've tried it on - but fails on this one server.
How can I go about debugging this? The error handler is never hit.
This does not answer your question but rather provides an alternative solution to setting the active printer.
You can use something like this to get the printer names:
Public Function GetPrinterNames() As Collection
Dim coll As New Collection
Dim i As Long
'
On Error Resume Next
With CreateObject("WScript.Network")
For i = 1 To .EnumPrinterConnections.Count Step 2
coll.Add .EnumPrinterConnections(i)
Next
End With
On Error GoTo 0
Set GetPrinterNames = coll
End Function
Note that the above does NOT give you the port number but that is not really necessary as you could use something like this to set the printer:
'*******************************************************************************
'Sets the ActivePrinter without requiring the winspool port number
'*******************************************************************************
Public Function SetPrinter(ByVal printerName As String) As Boolean
If LenB(printerName) = 0 Then Exit Function
Dim i As Long
'
On Error Resume Next
Application.ActivePrinter = printerName
If Err.Number = 0 Then
SetPrinter = True
Exit Function
End If
Err.Clear
For i = 0 To 99
Application.ActivePrinter = printerName & " on NE" & Format$(i, "00:")
If Err.Number = 0 Then
SetPrinter = True
Exit Function
End If
Err.Clear
Next i
On Error GoTo 0
End Function

Excel VBA function can't set range

I've written a VBA function that takes two parameters, the first is a string and the 2nd is a range, specified in the sheet as:
=strPack(B1,G3)
In the code, this routine is declared as:
Public Function strPack(ByVal strHex As String, ByRef rngCnt As Range) As String
On Error Goto ErrHandler
If False Then
ErrHandler:
MsgBox Err.Description
Exit Function
End If
Dim intCnt As Integer
intCnt = 0
'...do something with strHex and increment intCnt whilst we go
rngCnt.Value = CStr(intCnt)
'strPack is populated by the body of the function
strPack = "Hello World"
End Function
I've tried .Value, .Value2 and .Text, all result in an error:
Application-defined or object-defined error
When I look in the debugger, both strHex and rngCnt are valid and correct. Why can't I assign to the range and how do I fix it?
The error handler is not the problem, try it out, it works perfectly well and is a standard way of picking up errors and aborting a function when an error occurs.
[Edit] I've just tried the following:
Public Sub updateCount()
Worksheets("Sheet1").Range("G3").Value = CStr(intProcessed)
End Sub
intProcessed is global to the module and is an integer, result is the same, exactly the same error.
[Edit2] I want to remove this post as I've changed the approach now to call another subroutine that returns a value which is dropped into the cell. I can't delete it! Thank you to all for your help.
See the code comments:
Public Function strPack(ByVal strHex As String, ByVal rngCnt As Range) As String
Dim lRes As Long
On Error GoTo errHandler
lRes = 1000 '==> Your business logic goes here
'/ This is the gymnastics you do to update range from an UDF
Application.Evaluate ("UpdateRange(" & rngCnt.Address & "," & lRes & ")")
strPack = "SUCCESSFULL"
errHandler:
If Err.Number <> 0 Then
strPack = "FAILED"
End If
End Function
'/ Helper to allow range update from UDF
Private Function UpdateRange(rngDest As Range, val As Variant)
rngDest.Value = val
End Function

Accessing open workbook in a sub generates Error 1004 "Method of 'Sheets' of Object '_Global' not failed" sometimes

I am getting inconsistent results when I try to refer to an active workbook. About half the time I get the "Method of 'Sheets' of Object '_Global' not failed" error and other times the code works fine. I don't see a pattern.
The VBA code is part of a Word document that allows the user to open a template Excel file and select/copy text from the Word doc into rows on the Excel file.
In a previous sub I successfully open an Excel template file (I call it a RTM template). In the code below I want to activate the "RTM" worksheet, select the first cell where the template could already have data in it from a previous execution and if there is, then count how many rows of data exist. In this way the new data will be posted in the first row which does not have any data. I am using named ranges in my Workbook to refer to the starting cell ("First_Cell_For_Data").
When I run my code sometimes it runs without error and other times it stops on the "Sheets("RTM").Activate" and gives me the "Method...." error. The same result occurs when I change the variable definition of wb_open to Object. I have also tried using "wb_open.Sheets("RTM").Activate" with the same results.
As suggested in the comments below I added "If wb_open is nothing ...." to debug the issue. I also added the sub List_Open_Workbooks which enumerates the open workbooks (of which there is only 1) and activates the one that matches the name of the one with the correct filename. This is successful. But upon returning to Check_Excel_RTM_Template I still get the Method error on the "Sheets("RTM").Activate" line.
Second Update: after more time diagnosing the problem (which still occurs intermittently) I have added some code that may help getting to the root of the problem. In the "List_Open_Workbooks" sub I test for xlApp.Workbooks.Count = 0. So all references to an open Excel workbook will fail. At this point my template workbook is open in Windows. Am I drawing the correct conclusion?
Third Update: I tried Set wb_open = GetObject(str_filename) where str_filename contains the name of the Excel template file I just opened.
I get the following error message.
Also, I noticed that if I start with a fresh launch of Word and Excel it seems to run just fine.
Sub Check_Excel_RTM_Template(b_Excel_File_Has_Data As Boolean, i_rows_of_data As Integer)
Dim i_starting_row_for_data As Integer
Dim wb_open As Object
Set wb_open = ActiveWorkbook
i_rows_of_data = 0
If wb_open Is Nothing Then
MsgBox "RTM Workbook not open in Check_Excel_RTM_Template"
Call List_Open_Workbooks(b_Excel_File_Has_Data, i_rows_of_data)
Else
' On Error GoTo Err1:
' Sheets("RTM").Activate
' range("First_Cell_For_Data").Select
Workbooks(wb_open.Name).Worksheets("RTM").range("First_Cell_For_Data").Select
If Trim(ActiveCell.Value) <> "" Then
b_Excel_File_Has_Data = True
Do Until Trim(ActiveCell.Value) = ""
ActiveCell.Offset(1, 0).Select
i_rows_of_data = i_rows_of_data + 1
Loop
Else
b_Excel_File_Has_Data = False
End If
End If
Exit Sub
Err1:
MsgBox getName(str_Excel_Filename) & " is not a RTM template file."
b_abort = True
End Sub
Sub to enumerate all open workbooks
Sub List_Open_Workbooks(b_Excel_File_Has_Data As Boolean, i_rows_of_data As Integer)
Dim xlApp As Excel.Application
Set xlApp = GetObject(, "Excel.Application")
Dim str_filename As String
Dim xlWB As Excel.Workbook
If xlApp.Workbooks.Count = 0 Then
MsgBox "Error: Windows thinks there are no workbooks open in List_Open_Workbooks"
b_abort = True
Exit Sub
End If
For Each xlWB In xlApp.Workbooks
Debug.Print xlWB.Name
str_filename = getName(str_Excel_Filename)
If Trim(xlWB.Name) = Trim(str_filename) Then
xlWB.Activate
If xlWB Is Nothing Then
MsgBox "Workbook still not active in List_Open_Workbooks"
b_abort = True
Exit Sub
Else
' Sheets("RTM").Activate
Workbooks(xlWB.Name).Worksheets("RTM").range("First_Cell_For_Data").Select
range("First_Cell_For_Data").Select
If Trim(ActiveCell.Value) <> "" Then
b_Excel_File_Has_Data = True
Do Until Trim(ActiveCell.Value) = ""
ActiveCell.Offset(1, 0).Select
i_rows_of_data = i_rows_of_data + 1
Loop
Else
b_Excel_File_Has_Data = False
End If
End If
End If
Next xlWB
Set xlApp = Nothing
Set xlWB = Nothing
End Sub
Function to extract filename from path/filename
Function getName(pf)
getName = Split(Mid(pf, InStrRev(pf, "\") + 1), ".")(0) & ".xlsx"
End Function
I am hoping I found the source of my problem and solved it.
I believe that referring to an open workbook in sub using Dim wb_open As Object & Set wb_open = ActiveWorkbook in the Check_Excel_RTM_Template sub is causing my inconsistent problems....perhaps this is an anomoly (bug) in the VBA implementation in Word.
In the revised code I posted below I am passing the o_Excel object from the calling routine and using oExcel.Activesheet.xxx to reference ranges and values.
Now I next problem is that I am having errors on the form control button code which also uses the Dim wb_open As Object & Set wb_open = ActiveWorkbook approach to referring to the open workbook. But I'll post that as a new question.
Thanks to all who commented and provided suggestions.
Sub Check_Excel_RTM_Template(oExcel As Object)
Dim i_starting_row_for_data As Integer
Dim str_filename As String
i_rows_of_data = 0
On Error GoTo Err1:
oExcel.ActiveSheet.range("First_Cell_For_Data").Select
If Trim(oExcel.ActiveCell.Value) <> "" Then
b_Excel_File_Has_Data = True
Do Until Trim(oExcel.ActiveCell.Value) = ""
oExcel.ActiveCell.Offset(1, 0).Select
i_rows_of_data = i_rows_of_data + 1
Loop
Else
b_Excel_File_Has_Data = False
End If
Exit Sub
Err1:
Documents(str_doc_index).Activate
MsgBox getName(str_Excel_Filename) & " is not a RTM template file."
b_abort = True
End Sub

Run-time error '-2147418113 (8000ffff)': Automation Error

My code is running perfectly until get to "If c = "" Then". At this point the run time error '-2147418113 (8000ffff)': Automation Error is raised. I have put an On Error Resume Next statement to check if everything goes right if I skip this line and it does. I really don't understand what this error means and I wasn't able to find useful information about it. Could someone bring light to this problem? I have tried to change "If c = "" Then" to "If len(c.value)=0 Then" but it raises the same error. All the variables in AddresRawDataFile are defined as public (as range) and they were set to range in a different module, called PublicVariable, which is called by every procedure.
Private Sub CommandButton3_Ok_Click()
Dim MsgAlert As String
Dim MsgBoxAlert As Variant 'Message box for for many checks done below
Dim c As Variant 'Variable used in a for each structure
Dim AddressRawDataFile As Variant 'Array of variables with address in Box2_UPb_Options
'Code to assign values from Box2_UPb_Options to the related variables
AddressRawDataFile = Array(RawHg202Range, RawPb204Range, RawPb206Range, RawPb207Range, RawPb208Range, RawTh232Range, RawU238Range, _
RawHg202Header, RawPb204HeaderRange, RawPb206HeaderRange, RawPb207HeaderRange, RawPb208HeaderRange, RawTh232HeaderRange, _
RawU238HeaderRange)
'All of the above variables must not be = ""
For Each c In AddressRawDataFile
'On Error Resume Next
If c = "" Then
MsgBoxAlert = MsgBox("There are one or more addresses missing in Start-AND-Options sheet. " & _
"Please, check it.", vbOKOnly, "Missing Address")
Load Box2_UPb_Options
Box2_UPb_Options.MultiPage1.Value = 2
Box2_UPb_Options.Show
End If
Next
As the items named RawHg202Range etc are actually Range objects then you should use Is Nothing to check if they are empty:
AddressRawDataFile = Array(RawHg202Range, RawPb204Range, RawPb206Range, RawPb207Range, RawPb208Range, RawTh232Range, RawU238Range, _
RawHg202Header, RawPb204HeaderRange, RawPb206HeaderRange, RawPb207HeaderRange, RawPb208HeaderRange, RawTh232HeaderRange, _
RawU238HeaderRange)
For Each c In AddressRawDataFile
If c Is Nothing Then

Properly Handling Errors in VBA (Excel)

I've been working with VBA for quite a while now, but I'm still not so sure about Error Handling.
A good article is the one of
CPearson.com
However I'm still wondering if the way I used to do ErrorHandling was/is completely wrong:
Block 1
On Error Goto ErrCatcher
If UBound(.sortedDates) > 0 Then
// Code
Else
ErrCatcher:
// Code
End If
The if clause, because if it is true, will be executed and if it fails the Goto will go into the Else-part, since the Ubound of an Array should never be zero or less, without an Error, this method worked quite well so far.
If I understood it right it should be like this:
Block 2
On Error Goto ErrCatcher
If Ubound(.sortedDates) > 0 Then
// Code
End If
Goto hereX
ErrCatcher:
//Code
Resume / Resume Next / Resume hereX
hereX:
Or even like this:
Block 3
On Error Goto ErrCatcher
If Ubound(.sortedDates) > 0 Then
// Code
End If
ErrCatcher:
If Err.Number <> 0 then
//Code
End If
The most common way I see is that one, that the Error "Catcher" is at the end of a sub and the Sub actually ends before with a "Exit Sub", but however isn't it a little confusing if the Sub is quite big if you jump vice versa to read through the code?
Block 4
Source of the following Code:
CPearson.com
On Error Goto ErrHandler:
N = 1 / 0 ' cause an error
'
' more code
'
Exit Sub
ErrHandler:
' error handling code'
Resume Next
End Sub
Should it be like in Block 3 ?
You've got one truly marvelous answer from ray023, but your comment that it's probably overkill is apt. For a "lighter" version....
Block 1 is, IMHO, bad practice. As already pointed out by osknows, mixing error-handling with normal-path code is Not Good. For one thing, if a new error is thrown while there's an Error condition in effect you will not get an opportunity to handle it (unless you're calling from a routine that also has an error handler, where the execution will "bubble up").
Block 2 looks like an imitation of a Try/Catch block. It should be okay, but it's not The VBA Way. Block 3 is a variation on Block 2.
Block 4 is a bare-bones version of The VBA Way. I would strongly advise using it, or something like it, because it's what any other VBA programmer inherting the code will expect. Let me present a small expansion, though:
Private Sub DoSomething()
On Error GoTo ErrHandler
'Dim as required
'functional code that might throw errors
ExitSub:
'any always-execute (cleanup?) code goes here -- analagous to a Finally block.
'don't forget to do this -- you don't want to fall into error handling when there's no error
Exit Sub
ErrHandler:
'can Select Case on Err.Number if there are any you want to handle specially
'display to user
MsgBox "Something's wrong: " & vbCrLf & Err.Description
'or use a central DisplayErr routine, written Public in a Module
DisplayErr Err.Number, Err.Description
Resume ExitSub
Resume
End Sub
Note that second Resume. This is a trick I learned recently: It will never execute in normal processing, since the Resume <label> statement will send the execution elsewhere. It can be a godsend for debugging, though. When you get an error notification, choose Debug (or press Ctl-Break, then choose Debug when you get the "Execution was interrupted" message). The next (highlighted) statement will be either the MsgBox or the following statement. Use "Set Next Statement" (Ctl-F9) to highlight the bare Resume, then press F8. This will show you exactly where the error was thrown.
As to your objection to this format "jumping around", A) it's what VBA programmers expect, as stated previously, & B) your routines should be short enough that it's not far to jump.
Two main purposes for error handling:
Trap errors you can
predict but can't control the user
from doing (e.g. saving a file to a
thumb drive when the thumb drives
has been removed)
For unexpected errors, present user with a form
that informs them what the problem
is. That way, they can relay that
message to you and you might be able
to give them a work-around while you
work on a fix.
So, how would you do this?
First of all, create an error form to display when an unexpected error occurs.
It could look something like this (FYI: Mine is called frmErrors):
Notice the following labels:
lblHeadline
lblSource
lblProblem
lblResponse
Also, the standard command buttons:
Ignore
Retry
Cancel
There's nothing spectacular in the code for this form:
Option Explicit
Private Sub cmdCancel_Click()
Me.Tag = CMD_CANCEL
Me.Hide
End Sub
Private Sub cmdIgnore_Click()
Me.Tag = CMD_IGNORE
Me.Hide
End Sub
Private Sub cmdRetry_Click()
Me.Tag = CMD_RETRY
Me.Hide
End Sub
Private Sub UserForm_Initialize()
Me.lblErrorTitle.Caption = "Custom Error Title Caption String"
End Sub
Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
'Prevent user from closing with the Close box in the title bar.
If CloseMode <> 1 Then
cmdCancel_Click
End If
End Sub
Basically, you want to know which button the user pressed when the form closes.
Next, create an Error Handler Module that will be used throughout your VBA app:
'****************************************************************
' MODULE: ErrorHandler
'
' PURPOSE: A VBA Error Handling routine to handle
' any unexpected errors
'
' Date: Name: Description:
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
'03/22/2010 Ray Initial Creation
'****************************************************************
Option Explicit
Global Const CMD_RETRY = 0
Global Const CMD_IGNORE = 1
Global Const CMD_CANCEL = 2
Global Const CMD_CONTINUE = 3
Type ErrorType
iErrNum As Long
sHeadline As String
sProblemMsg As String
sResponseMsg As String
sErrorSource As String
sErrorDescription As String
iBtnCap(3) As Integer
iBitmap As Integer
End Type
Global gEStruc As ErrorType
Sub EmptyErrStruc_S(utEStruc As ErrorType)
Dim i As Integer
utEStruc.iErrNum = 0
utEStruc.sHeadline = ""
utEStruc.sProblemMsg = ""
utEStruc.sResponseMsg = ""
utEStruc.sErrorSource = ""
For i = 0 To 2
utEStruc.iBtnCap(i) = -1
Next
utEStruc.iBitmap = 1
End Sub
Function FillErrorStruct_F(EStruc As ErrorType) As Boolean
'Must save error text before starting new error handler
'in case we need it later
EStruc.sProblemMsg = Error(EStruc.iErrNum)
On Error GoTo vbDefaultFill
EStruc.sHeadline = "Error " & Format$(EStruc.iErrNum)
EStruc.sProblemMsg = EStruc.sErrorDescription
EStruc.sErrorSource = EStruc.sErrorSource
EStruc.sResponseMsg = "Contact the Company and tell them you received Error # " & Str$(EStruc.iErrNum) & ". You should write down the program function you were using, the record you were working with, and what you were doing."
Select Case EStruc.iErrNum
'Case Error number here
'not sure what numeric errors user will ecounter, but can be implemented here
'e.g.
'EStruc.sHeadline = "Error 3265"
'EStruc.sResponseMsg = "Contact tech support. Tell them what you were doing in the program."
Case Else
EStruc.sHeadline = "Error " & Format$(EStruc.iErrNum) & ": " & EStruc.sErrorDescription
EStruc.sProblemMsg = EStruc.sErrorDescription
End Select
GoTo FillStrucEnd
vbDefaultFill:
'Error Not on file
EStruc.sHeadline = "Error " & Format$(EStruc.iErrNum) & ": Contact Tech Support"
EStruc.sResponseMsg = "Contact the Company and tell them you received Error # " & Str$(EStruc.iErrNum)
FillStrucEnd:
Exit Function
End Function
Function iErrorHandler_F(utEStruc As ErrorType) As Integer
Static sCaption(3) As String
Dim i As Integer
Dim iMCursor As Integer
Beep
'Setup static array
If Len(sCaption(0)) < 1 Then
sCaption(CMD_IGNORE) = "&Ignore"
sCaption(CMD_RETRY) = "&Retry"
sCaption(CMD_CANCEL) = "&Cancel"
sCaption(CMD_CONTINUE) = "Continue"
End If
Load frmErrors
'Did caller pass error info? If not fill struc with the needed info
If Len(utEStruc.sHeadline) < 1 Then
i = FillErrorStruct_F(utEStruc)
End If
frmErrors!lblHeadline.Caption = utEStruc.sHeadline
frmErrors!lblProblem.Caption = utEStruc.sProblemMsg
frmErrors!lblSource.Caption = utEStruc.sErrorSource
frmErrors!lblResponse.Caption = utEStruc.sResponseMsg
frmErrors.Show
iErrorHandler_F = frmErrors.Tag ' Save user response
Unload frmErrors ' Unload and release form
EmptyErrStruc_S utEStruc ' Release memory
End Function
You may have errors that will be custom only to your application. This would typically be a short list of errors specifically only to your application.
If you don't already have a constants module, create one that will contain an ENUM of your custom errors. (NOTE: Office '97 does NOT support ENUMS.). The ENUM should look something like this:
Public Enum CustomErrorName
MaskedFilterNotSupported
InvalidMonthNumber
End Enum
Create a module that will throw your custom errors.
'********************************************************************************************************************************
' MODULE: CustomErrorList
'
' PURPOSE: For trapping custom errors applicable to this application
'
'INSTRUCTIONS: To use this module to create your own custom error:
' 1. Add the Name of the Error to the CustomErrorName Enum
' 2. Add a Case Statement to the raiseCustomError Sub
' 3. Call the raiseCustomError Sub in the routine you may see the custom error
' 4. Make sure the routine you call the raiseCustomError has error handling in it
'
'
' Date: Name: Description:
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
'03/26/2010 Ray Initial Creation
'********************************************************************************************************************************
Option Explicit
Const MICROSOFT_OFFSET = 512 'Microsoft reserves error values between vbObjectError and vbObjectError + 512
'************************************************************************************************
' FUNCTION: raiseCustomError
'
' PURPOSE: Raises a custom error based on the information passed
'
'PARAMETERS: customError - An integer of type CustomErrorName Enum that defines the custom error
' errorSource - The place the error came from
'
' Returns: The ASCII vaule that should be used for the Keypress
'
' Date: Name: Description:
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
'03/26/2010 Ray Initial Creation
'************************************************************************************************
Public Sub raiseCustomError(customError As Integer, Optional errorSource As String = "")
Dim errorLong As Long
Dim errorDescription As String
errorLong = vbObjectError + MICROSOFT_OFFSET + customError
Select Case customError
Case CustomErrorName.MaskedFilterNotSupported
errorDescription = "The mask filter passed is not supported"
Case CustomErrorName.InvalidMonthNumber
errorDescription = "Invalid Month Number Passed"
Case Else
errorDescription = "The custom error raised is unknown."
End Select
Err.Raise errorLong, errorSource, errorDescription
End Sub
You are now well equipped to trap errors in your program. You sub (or function), should look something like this:
Public Sub MySub(monthNumber as Integer)
On Error GoTo eh
Dim sheetWorkSheet As Worksheet
'Run Some code here
'************************************************
'* OPTIONAL BLOCK 1: Look for a specific error
'************************************************
'Temporarily Turn off Error Handling so that you can check for specific error
On Error Resume Next
'Do some code where you might expect an error. Example below:
Const ERR_SHEET_NOT_FOUND = 9 'This error number is actually subscript out of range, but for this example means the worksheet was not found
Set sheetWorkSheet = Sheets("January")
'Now see if the expected error exists
If Err.Number = ERR_SHEET_NOT_FOUND Then
MsgBox "Hey! The January worksheet is missing. You need to recreate it."
Exit Sub
ElseIf Err.Number <> 0 Then
'Uh oh...there was an error we did not expect so just run basic error handling
GoTo eh
End If
'Finished with predictable errors, turn basic error handling back on:
On Error GoTo eh
'**********************************************************************************
'* End of OPTIONAL BLOCK 1
'**********************************************************************************
'**********************************************************************************
'* OPTIONAL BLOCK 2: Raise (a.k.a. "Throw") a Custom Error if applicable
'**********************************************************************************
If not (monthNumber >=1 and monthnumber <=12) then
raiseCustomError CustomErrorName.InvalidMonthNumber, "My Sub"
end if
'**********************************************************************************
'* End of OPTIONAL BLOCK 2
'**********************************************************************************
'Rest of code in your sub
goto sub_exit
eh:
gEStruc.iErrNum = Err.Number
gEStruc.sErrorDescription = Err.Description
gEStruc.sErrorSource = Err.Source
m_rc = iErrorHandler_F(gEStruc)
If m_rc = CMD_RETRY Then
Resume
End If
sub_exit:
'Any final processing you want to do.
'Be careful with what you put here because if it errors out, the error rolls up. This can be difficult to debug; especially if calling routine has no error handling.
Exit Sub 'I was told a long time ago (10+ years) that exit sub was better than end sub...I can't tell you why, so you may not want to put in this line of code. It's habit I can't break :P
End Sub
A copy/paste of the code above may not work right out of the gate, but should definitely give you the gist.
I definitely wouldn't use Block1. It doesn't seem right having the Error block in an IF statement unrelated to Errors.
Blocks 2,3 & 4 I guess are variations of a theme. I prefer the use of Blocks 3 & 4 over 2 only because of a dislike of the GOTO statement; I generally use the Block4 method. This is one example of code I use to check if the Microsoft ActiveX Data Objects 2.8 Library is added and if not add or use an earlier version if 2.8 is not available.
Option Explicit
Public booRefAdded As Boolean 'one time check for references
Public Sub Add_References()
Dim lngDLLmsadoFIND As Long
If Not booRefAdded Then
lngDLLmsadoFIND = 28 ' load msado28.tlb, if cannot find step down versions until found
On Error GoTo RefErr:
'Add Microsoft ActiveX Data Objects 2.8
Application.VBE.ActiveVBProject.references.AddFromFile _
Environ("CommonProgramFiles") + "\System\ado\msado" & lngDLLmsadoFIND & ".tlb"
On Error GoTo 0
Exit Sub
RefErr:
Select Case Err.Number
Case 0
'no error
Case 1004
'Enable Trust Centre Settings
MsgBox ("Certain VBA References are not available, to allow access follow these steps" & Chr(10) & _
"Goto Excel Options/Trust Centre/Trust Centre Security/Macro Settings" & Chr(10) & _
"1. Tick - 'Disable all macros with notification'" & Chr(10) & _
"2. Tick - 'Trust access to the VBA project objects model'")
End
Case 32813
'Err.Number 32813 means reference already added
Case 48
'Reference doesn't exist
If lngDLLmsadoFIND = 0 Then
MsgBox ("Cannot Find Required Reference")
End
Else
For lngDLLmsadoFIND = lngDLLmsadoFIND - 1 To 0 Step -1
Resume
Next lngDLLmsadoFIND
End If
Case Else
MsgBox Err.Number & vbCrLf & Err.Description, vbCritical, "Error!"
End
End Select
On Error GoTo 0
End If
booRefAdded = TRUE
End Sub
I keep things simple:
At the module level I define two variables and set one to the name of the module itself.
Private Const ThisModuleName As String = "mod_Custom_Functions"
Public sLocalErrorMsg As String
Within each Sub/Function of the module I define a local variable
Dim ThisRoutineName As String
I set ThisRoutineName to the name of the sub or function
' Housekeeping
On Error Goto ERR_RTN
ThisRoutineName = "CopyWorksheet"
I then send all errors to an ERR_RTN: when they occur, but I first set the sLocalErrorMsg to define what the error actually is and provide some debugging info.
If Len(Trim(FromWorksheetName)) < 1 Then
sLocalErrorMsg = "Parameter 'FromWorksheetName' Is Missing."
GoTo ERR_RTN
End If
At the bottom of each sub/function, I direct the logic flow as follows
'
' The "normal" logic goes here for what the routine does
'
GoTo EXIT_RTN
ERR_RTN:
On Error Resume Next
' Call error handler if we went this far.
ErrorHandler ThisModuleName, ThisRoutineName, sLocalErrorMsg, Err.Description, Err.Number, False
EXIT_RTN:
On Error Resume Next
'
' Some closing logic
'
End If
I then have a seperate module I put in all projects called "mod_Error_Handler".
'
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Subroutine Name: ErrorHandler '
' '
' Description: '
' This module will handle the common error alerts. '
' '
' Inputs: '
' ModuleName String 'The name of the module error is in. '
' RoutineName String 'The name of the routine error in in. '
' LocalErrorMsg String 'A local message to assist with troubleshooting.'
' ERRDescription String 'The Windows Error Description. '
' ERRCode Long 'The Windows Error Code. '
' Terminate Boolean 'End program if error encountered? '
' '
' Revision History: '
' Date (YYYYMMDD) Author Change '
' =============== ===================== =============================================== '
' 20140529 XXXXX X. XXXXX Original '
' '
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
'
Public Sub ErrorHandler(ModuleName As String, RoutineName As String, LocalErrorMsg As String, ERRDescription As String, ERRCode As Long, Terminate As Boolean)
Dim sBuildErrorMsg As String
' Build Error Message To Display
sBuildErrorMsg = "Error Information:" & vbCrLf & vbCrLf
If Len(Trim(ModuleName)) < 1 Then
ModuleName = "Unknown"
End If
If Len(Trim(RoutineName)) < 1 Then
RoutineName = "Unknown"
End If
sBuildErrorMsg = sBuildErrorMsg & "Module Name: " & ModuleName & vbCrLf & vbCrLf
sBuildErrorMsg = sBuildErrorMsg & "Routine Name: " & RoutineName & vbCrLf & vbCrLf
If Len(Trim(LocalErrorMsg)) > 0 Then
sBuildErrorMsg = sBuildErrorMsg & "Local Error Msg: " & LocalErrorMsg & vbCrLf & vbCrLf
End If
If Len(Trim(ERRDescription)) > 0 Then
sBuildErrorMsg = sBuildErrorMsg & "Program Error Msg: " & ERRDescription & vbCrLf & vbCrLf
If IsNumeric(ERRCode) Then
sBuildErrorMsg = sBuildErrorMsg & "Program Error Code: " & Trim(Str(ERRCode)) & vbCrLf & vbCrLf
End If
End If
MsgBox sBuildErrorMsg, vbOKOnly + vbExclamation, "Error Detected!"
If Terminate Then
End
End If
End Sub
The end result is a pop-up error message teling me in what module, what soubroutine, and what the error message specifically was. In addition, it also will insert the Windows error message and code.
Block 2 doesn't work because it doesn't reset the Error Handler potentially causing an endless loop. For Error Handling to work properly in VBA, you need a Resume statement to clear the Error Handler. The Resume also reactivates the previous Error Handler. Block 2 fails because a new error would go back to the previous Error Handler causing an infinite loop.
Block 3 fails because there is no Resume statement so any attempt at error handling after that will fail.
Every error handler must be ended by exiting the procedure or a Resume statement. Routing normal execution around an error handler is confusing. This is why error handlers are usually at the bottom.
But here is another way to handle an error in VBA. It handles the error inline like Try/Catch in VB.net There are a few pitfalls, but properly managed it works quite nicely.
Sub InLineErrorHandling()
'code without error handling
BeginTry1:
'activate inline error handler
On Error GoTo ErrHandler1
'code block that may result in an error
Dim a As String: a = "Abc"
Dim c As Integer: c = a 'type mismatch
ErrHandler1:
'handle the error
If Err.Number <> 0 Then
'the error handler has deactivated the previous error handler
MsgBox (Err.Description)
'Resume (or exit procedure) is the only way to get out of an error handling block
'otherwise the following On Error statements will have no effect
'CAUTION: it also reactivates the previous error handler
Resume EndTry1
End If
EndTry1:
'CAUTION: since the Resume statement reactivates the previous error handler
'you must ALWAYS use an On Error GoTo statement here
'because another error here would cause an endless loop
'use On Error GoTo 0 or On Error GoTo <Label>
On Error GoTo 0
'more code with or without error handling
End Sub
Sources:
http://www.cpearson.com/excel/errorhandling.htm
http://msdn.microsoft.com/en-us/library/bb258159.aspx
The key to making this work is to use a Resume statement immediately followed by another On Error statement. The Resume is within the error handler and diverts code to the EndTry1 label. You must immediately set another On Error statement to avoid problems as the previous error handler will "resume". That is, it will be active and ready to handle another error. That could cause the error to repeat and enter an infinite loop.
To avoid using the previous error handler again you need to set On Error to a new error handler or simply use On Error Goto 0 to cancel all error handling.
This is what I'm teaching my students tomorrow. After years of looking at this stuff... ie all of the documentation above http://www.cpearson.com/excel/errorhandling.htm comes to mind as an excellent one...
I hope this summarizes it for others. There is an Err object and an active (or inactive) ErrorHandler. Both need to be handled and reset for new errors.
Paste this into a workbook and step through it with F8.
Sub ErrorHandlingDemonstration()
On Error GoTo ErrorHandler
'this will error
Debug.Print (1 / 0)
'this will also error
dummy = Application.WorksheetFunction.VLookup("not gonna find me", Range("A1:B2"), 2, True)
'silly error
Dummy2 = "string" * 50
Exit Sub
zeroDivisionErrorBlock:
maybeWe = "did some cleanup on variables that shouldnt have been divided!"
' moves the code execution to the line AFTER the one that errored
Resume Next
vlookupFailedErrorBlock:
maybeThisTime = "we made sure the value we were looking for was in the range!"
' moves the code execution to the line AFTER the one that errored
Resume Next
catchAllUnhandledErrors:
MsgBox(thisErrorsDescription)
Exit Sub
ErrorHandler:
thisErrorsNumberBeforeReset = Err.Number
thisErrorsDescription = Err.Description
'this will reset the error object and error handling
On Error GoTo 0
'this will tell vba where to go for new errors, ie the new ErrorHandler that was previous just reset!
On Error GoTo ErrorHandler
' 11 is the err.number for division by 0
If thisErrorsNumberBeforeReset = 11 Then
GoTo zeroDivisionErrorBlock
' 1004 is the err.number for vlookup failing
ElseIf thisErrorsNumberBeforeReset = 1004 Then
GoTo vlookupFailedErrorBlock
Else
GoTo catchAllUnhandledErrors
End If
End Sub

Resources