VBA Excel Error Handling - especially in functions - Professional Excel Development Style - excel
I got the book "Professional Excel Development" by Rob Bovey and it is opening up my eyes.
I am refitting my code with error handling. However, there is a lot I don't understand. I especially need to know how to correctly use it in functions. I use Bovey's rethrow version of the error handler (at bottom). When I started, I was using the basic boolean (non-rethrow) method and turned my subroutines into boolean functions. (P.S. I am switching back to the boolean method based on the answer.)
I need guidance on how to fit functions into this scheme. I want them to return their real values (a string or double, e.g., or -1 if they fail in some cases) so I can nest them in other functions and not just return an error handling boolean.
This is what a typical subroutine call to bDrawCellBorders(myWS) would look like within an entry point. Sub calls seem to be working well. (I.e. it is a subroutine that was turned into a function only so it can return a boolean to the error handling scheme.)
Sub UpdateMe() ' Entry Point
Const sSOURCE As String = "UpdateMe()"
On Error GoTo ErrorHandler
Set myWS = ActiveCell.Worksheet
Set myRange = ActiveCell
myWS.Unprotect
' lots of code
If Not bDrawCellBorders(myWS) Then ERR.Raise glHANDLED_ERROR ' Call subroutine
' lots of code
ErrorExit:
On Error Resume Next
Application.EnableEvents = True
myWS.Protect AllowFormattingColumns:=True
Exit Sub
ErrorHandler:
If bCentralErrorHandler(msMODULE, sSOURCE,,True) Then ' Call as Entry Point
Stop
Resume
Else
Resume ErrorExit
End If
End Sub
However, I don't know how to extend this to real functions. This is based off an example in the book that was drawn up for a subroutine, and I just switched it to a function.
Questions:
* How do I call it? Is it simply like x = sngDoSomeMath(17)
* Will its error handling function properly?
* Where is the right place or places to call the error handling routine with bReThrow=true?
Public Function sngDoSomeMath(ByVal iNum As Integer) As Single
Dim sngResult As Single
Const sSOURCE As String = "sngDoSomeMath()"
On Error GoTo ErrorHandler
' example 1, input did not pass validation. don't want to
' go up the error stack but just inform the
' calling program that they didn't get a good result from this
' function call so they can do something else
If iNum <> 42 Then
sngResult = -1 'function failed because I only like the number 42
GoTo ExitHere
End If
' example 2, true error generated
sngResult = iNum / 0
sngDoSomeMath = lResult
ExitHere:
Exit Function
ErrorHandler:
' Run cleanup code
' ... here if any
' Then do error handling
If bCentralErrorHandler(msMODULE, sSOURCE, , , True) Then ' The true is for RETHROW
Stop
Resume
End If
End Function
The Error Handler Routine:
'
' Description: This module contains the central error
' handler and related constant declarations.
'
' Authors: Rob Bovey, www.appspro.com
' Stephen Bullen, www.oaltd.co.uk
'
' Chapter Change Overview
' Ch# Comment
' --------------------------------------------------------------
' 15 Initial version
'
Option Explicit
Option Private Module
' **************************************************************
' Global Constant Declarations Follow
' **************************************************************
Public Const gbDEBUG_MODE As Boolean = False ' True enables debug mode, False disables it.
Public Const glHANDLED_ERROR As Long = 9999 ' Run-time error number for our custom errors.
Public Const glUSER_CANCEL As Long = 18 ' The error number generated when the user cancels program execution.
' **************************************************************
' Module Constant Declarations Follow
' **************************************************************
Private Const msSILENT_ERROR As String = "UserCancel" ' Used by the central error handler to bail out silently on user cancel.
Private Const msFILE_ERROR_LOG As String = "Error.log" ' The name of the file where error messages will be logged to.
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
' Comments: This is the central error handling procedure for the
' program. It logs and displays any run-time errors
' that occur during program execution.
'
' Arguments: sModule The module in which the error occured.
' sProc The procedure in which the error occured.
' sFile (Optional) For multiple-workbook
' projects this is the name of the
' workbook in which the error occured.
' bEntryPoint (Optional) True if this call is
' being made from an entry point
' procedure. If so, an error message
' will be displayed to the user.
'
' Returns: Boolean True if the program is in debug
' mode, False if it is not.
'
' Date Developer Chap Action
' --------------------------------------------------------------
' 03/30/08 Rob Bovey Ch15 Initial version
'
Public Function bCentralErrorHandler( _
ByVal sModule As String, _
ByVal sProc As String, _
Optional ByVal sFile As String, _
Optional ByVal bEntryPoint As Boolean, _
Optional ByVal bReThrow As Boolean = True) As Boolean
Static sErrMsg As String
Dim iFile As Integer
Dim lErrNum As Long
Dim sFullSource As String
Dim sPath As String
Dim sLogText As String
' Grab the error info before it's cleared by
' On Error Resume Next below.
lErrNum = ERR.Number
' If this is a user cancel, set the silent error flag
' message. This will cause the error to be ignored.
If lErrNum = glUSER_CANCEL Then sErrMsg = msSILENT_ERROR
' If this is the originating error, the static error
' message variable will be empty. In that case, store
' the originating error message in the static variable.
If Len(sErrMsg) = 0 Then sErrMsg = ERR.Description
' We cannot allow errors in the central error handler.
On Error Resume Next
' Load the default filename if required.
If Len(sFile) = 0 Then sFile = ThisWorkbook.Name
' Get the application directory.
sPath = ThisWorkbook.Path
If Right$(sPath, 1) <> "\" Then sPath = sPath & "\"
' Construct the fully-qualified error source name.
sFullSource = "[" & sFile & "]" & sModule & "." & sProc
' Create the error text to be logged.
sLogText = " " & sFullSource & ", Error " & _
CStr(lErrNum) & ": " & sErrMsg
' Open the log file, write out the error information and
' close the log file.
iFile = FreeFile()
Open sPath & msFILE_ERROR_LOG For Append As #iFile
Print #iFile, Format$(Now(), "mm/dd/yy hh:mm:ss"); sLogText
If bEntryPoint Or Not bReThrow Then Print #iFile,
Close #iFile
' Do not display or debug silent errors.
If sErrMsg <> msSILENT_ERROR Then
' Show the error message when we reach the entry point
' procedure or immediately if we are in debug mode.
If bEntryPoint Or gbDEBUG_MODE Then
Application.ScreenUpdating = True
MsgBox sErrMsg, vbCritical, gsAPP_NAME
' Clear the static error message variable once
' we've reached the entry point so that we're ready
' to handle the next error.
sErrMsg = vbNullString
End If
' The return vale is the debug mode status.
bCentralErrorHandler = gbDEBUG_MODE
Else
' If this is a silent error, clear the static error
' message variable when we reach the entry point.
If bEntryPoint Then sErrMsg = vbNullString
bCentralErrorHandler = False
End If
'If we're using re-throw error handling,
'this is not the entry point and we're not debugging,
're-raise the error, to be caught in the next procedure
'up the call stack.
'Procedures that handle their own errors can call the
'central error handler with bReThrow = False to log the
'error, but not re-raise it.
If bReThrow Then
If Not bEntryPoint And Not gbDEBUG_MODE Then
On Error GoTo 0
ERR.Raise lErrNum, sFullSource, sErrMsg
End If
Else
'Error is being logged and handled,
'so clear the static error message variable
sErrMsg = vbNullString
End If
End Function
That is an amazing book by Rob.
My two cents of Error Handling (Either for a procedure or a Function) is based on KISS (Keep it simple Silly)
Understand what do you want from your error handler?
This is usually what I want/expect from my error handler...
Line on which the error happened
Error Number
Error Message
Reset Events if applicable
Lets break the above. As you are by now already aware how your error handler looks like, Consider this example.
Sub Sample()
Dim i As Integer, j As Integer
On Error GoTo Whoa
Application.ScreenUpdating = False
i = 1111111111
For j = 1 To i
Debug.Print ThisWorkbook.Sheets(1).Cells(i, 1).Value
Next i
LetsContinue:
Exit Sub
Whoa:
MsgBox Err.Description
Resume LetsContinue
End Sub
This is a very basic error handler but it's of very less help to me. So let's now tweak it to make it more useful. If you run the above code you get an error message like shown in the screenshot below and if you notice, it's not of much help.
Let's now tackle all the points that I mentioned in the Logic above
Line on which the error happened
There is a property called ERL which very few people are aware of. You can actually use it to get the line number of the code where the error happened. For that you have to ensure you number your code. See this example.
Sub Sample()
Dim i As Integer, j As Integer
10 On Error GoTo Whoa
20 Application.ScreenUpdating = False
30 i = 1111111111
40 For j = 1 To i
50 Debug.Print ThisWorkbook.Sheets(1).Cells(i, 1).Value
60 Next j
LetsContinue:
70 Exit Sub
Whoa:
80 MsgBox Erl
90 Resume LetsContinue
End Sub
When you run the above code, you will get this
So now I know that the error happened on Line 30 which is i = 1111111111
Moving on to next
Error Number
Error Message
The error number and the error message can be retrieved from Err.Number and Err.Description respectively. So now let's combine Erl, Err.Number and Err.Description
Check this example
Sub Sample()
Dim i As Integer, j As Integer
10 On Error GoTo Whoa
20 Application.ScreenUpdating = False
30 i = 1111111111
40 For j = 1 To i
50 Debug.Print ThisWorkbook.Sheets(1).Cells(i, 1).Value
60 Next j
LetsContinue:
70 Exit Sub
Whoa:
80 MsgBox "The Error Happened on Line : " & Erl & vbNewLine & _
"Error Message : " & Err.Description & vbNewLine & _
"Error Number : " & Err.Number
90 Resume LetsContinue
End Sub
When you run this code, you will get something like this.
You can choose to further customize the Error Message to make it more user friendly. For example
'~~> Message you want to deliver to the user in case the error happens
Const sMsg As String = "Please take a screenshot of this message and contact the developer for a resolution"
'~~> Title of your message box
Const sTitle As String = "Oopsie Daisies"
'~~> Change the above as applicable
Sub Sample()
Dim i As Integer, j As Integer
10 On Error GoTo Whoa
20 Application.ScreenUpdating = False
30 i = 1111111111
40 For j = 1 To i
50 Debug.Print ThisWorkbook.Sheets(1).Cells(i, 1).Value
60 Next j
LetsContinue:
70 Exit Sub
Whoa:
80 MsgBox "The Error Happened on Line : " & Erl & vbNewLine & _
"Error Message : " & Err.Description & vbNewLine & _
"Error Number : " & Err.Number & vbNewLine & vbNewLine & _
sMsg, vbCritical, sTitle
90 Resume LetsContinue
End Sub
On to the next one :)
Reset Events if applicable
When you are working with events and an error occurs, if there is no error handling, the code breaks. Unfortunately that doesn't reset the events. It is very important that you reset the events in the Error handler.
If you notice in the above code we are setting the Application.ScreenUpdating = False. When the code breaks, that event doesn't get reset. You will have to handle that in the Error handler LetsContinue in this case. See this example.
'~~> Message you want to deliver to the user in case the error happens
Const sMsg As String = "Please take a screenshot of this message and contact the developer for a resolution"
'~~> Title of your message box
Const sTitle As String = "Oopsie Daisies"
'~~> Change the above as applicable
Sub Sample()
Dim i As Integer, j As Integer
10 On Error GoTo Whoa
20 Application.ScreenUpdating = False
30 i = 1111111111
40 For j = 1 To i
50 Debug.Print ThisWorkbook.Sheets(1).Cells(i, 1).Value
60 Next j
LetsContinue:
70 Application.ScreenUpdating = True
80 Exit Sub
Whoa:
90 MsgBox "The Error Happened on Line : " & Erl & vbNewLine & _
"Error Message : " & Err.Description & vbNewLine & _
"Error Number : " & Err.Number & vbNewLine & vbNewLine & _
sMsg, vbCritical, sTitle
100 Resume LetsContinue
End Sub
Like Philippe, I also strongly suggest that you use MZ-Tools for VBA. I have been using it now for donkey years...
Hope this helps.
I needed a bit more help on this specific technique so I went right to the source and Mr. Bovey was gracious enough to reply. He gave me permission to post his response to the StackOverflow community.
The instructions below refer to his preferred method of error handling for functions the "boolean error handling" technique and not to the alternate "rethrow method", both described in his book "Professional Excel Development" 2nd edition.
Hi Shari,
In answer to your questions about error handling in functions, there are
three error handling scenarios you can have with a function in VBA:
1) The function is so trivial that is doesn't need an error handler. In the
unlikely event an error occurs in a function like this it will spill over
into the error handler of the calling procedure.
2) A non-trivial function needs an error handler and uses the Boolean return
value system described in the book. Any other values the function needs to
return are returned through ByRef arguments. This case covers the vast majority of
functions I write. There are some things you can't do with functions like
this, feeding them directly into the argument of another function is one
example, but I consider this a good tradeoff in order to achieve bullet
proof error handling.
3) A non-trivial function needs an error handler and must return a value not
related to its error status. This is a rare situation because I can convert
99% plus of these into case 2 by restructuring my code. If you can't do
this, your only choice is to select an arbitrary return value that is out of
the range of normal return values and use this to indicate that an error has
occurred. If the caller of this function sees this arbitrary error flag
value it knows it can't continue.
Rob Bovey
Application Professionals
http://www.appspro.com/
Code Example (Shari W)
' Show how to call a function using this error handling method.
Const giBAD_RESULT As Integer = -1
Function TestMath() ' An Entry Point
Dim sngResult As Single
Dim iNum As Integer
' Call the function, actual result goes in sngResult but it returns the error handling boolean.
' A true error like Div 0 will go to error handler.
' Set Up Error Handling for Entry Point
Application.EnableCancelKey = xlErrorHandler
Dim bUserCancel As Boolean
Const sSOURCE As String = "TestMath()"
On Error GoTo ErrorHandler
' End Error Set Up
iNum = 0 ' Try 0 to create error
If Not bDoSomeMath(iNum, sngResult) Then ERR.Raise glHANDLED_ERROR
' If function does parameter checking and wants to return a bad input code, check for that.
If sngResult = giBAD_RESULT Then
MsgBox ("Bad input to bDoSomeMath " & iNum)
Else
MsgBox ("I believe the answer is " & sngResult)
End If
ErrorExit:
On Error Resume Next
Exit Function
ErrorHandler:
If bCentralErrorHandler(msMODULE, sSOURCE, , True) Then
Stop
Resume
Else
Resume ErrorExit
End If
End Function
Function bDoSomeMath(ByVal iNum As Integer, ByRef sngResult As Single) As Boolean
' Error handling Set Up
Dim bReturn As Boolean
Const sSOURCE As String = "bDoSomeMath()"
On Error GoTo ErrorHandler
bReturn = True
' End Error Set Up
If iNum < 0 Or iNum > 1000 Then
sngResult = giBAD_RESULT 'function failed because I only like the numbers 0 to 1000
GoTo ErrorExit
Else
sngResult = 100 / iNum ' generate a true error by iNum = 0
End If
ErrorExit:
On Error Resume Next
bDoSomeMath = bReturn
Exit Function
ErrorHandler:
bReturn = False
If bCentralErrorHandler(msMODULE, sSOURCE, , , True) Then
Stop
Resume
Else
Resume ErrorExit
End If
End Function
a proposal for error handling management in VBA can be found here .
The very same tool (MZ-Tools) and method (standard/generic error handler, which could be used to build an automated error reporting system) will work with Excel.
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
Handling error of returning function in VBA
I'm struggling a little bit with handling the errors of a returning function in VBA. I got some code in a sub which calls a function. The function returns 0 if it succeeds and -1 if there is an error. This is my sub: Sub mySub Dim returnValue as Integer returnValue = functionA(...) If returnValue = -1 Then MsgBox "The following error appeared: " & err.description & ", Errornumber= " & err.number Else MsgBox "Success" End If End Sub My function looks like this: Function functionA(...) as Integer On error goto errorHandler ' do something funtionA = 0 Exit Function errorHandler: functionA = -1 End Function Now my problem: if the function returns -1 because it was stopped by an error, I cannot get any information from the error object in my calling sub. The errornumber is 0 and the description is emtpy. It seems like the end of the function resets the err object. Is there a smart way of achieving what I want to do? Thanks in advance! :)
One approach would be to return the error number instead of -1 and then using it to get the error information Sub mySub Dim returnValue as Integer returnValue = functionA(...) if returnValue <> 0 Then On Error Resume Next Err.Raise returnValue 'raise the same error that appeared in functionA to get its details MsgBox "The following error appeared: " & Err.Description & ", Errornumber= " & Err.Number On Error GoTo 0 Else MsgBox "Success" End If End Sub Function functionA(...) as Integer On error goto errorHandler ' do something funtionA = 0 Exit Function errorHandler: functionA = Err.Number End Function A shorter and nicer way is to do all the error handling in the calling procedure. For example: Sub mySub() Dim returnValue as Integer On Error Resume Next returnValue = functionA(...) If Err.Number <> 0 Then MsgBox "The following error appeared: " & err.description & ", Errornumber= " & err.number Else MsgBox "Success" End If End Sub The function then becomes Function functionA(...) as Integer ' do something End Function
A much better way of dealing with errors is by emulating the try/catch construct found in other languages. The VBA way of doing this is described in the RubberDuck article on 'Pattern: TryParse' https://rubberduckvba.wordpress.com/2019/05/09/pattern-tryparse/
Error handling between multiple modules and subroutines
I had a single module with a single working subroutine that was very long. I decided to chop it up into smaller chunks. I now have a half dozen modules and several of the modules contain multiple subroutines (like 6 or less). One module contains all my functions for example (like 10 functions). One subroutine (RT_CMM_DATA_COMPILER_MAIN) defines several variables and calls the other modules, like this: Sub RT_CMM_DATA_COMPILER_MAIN() Public Path As String Public wkbTemp As Workbook Public StartTime As Double Public CurrentDateTime As Date 'Etc.... 'Do some stuff.... Call RT_SETUP_WKBTEMP Call RT_COMPILE_TABLE_HEADER Call RT_RUN_LOG Call RT_SAVE_WORKBOOKS 'Do some stuff.... Application.Quit End Sub Sub LogError(ErrorMsg As String) wkbErrorLog.Activate On Error Resume Next Range("A" & LRow).Value = CurrentData_FilePath Range("B" & LRow).Value = CurrentDateTime Range("C" & LRow).Value = ErrorMsg 'Etc.... wkbErrorLog.Save wkbErrorLog.Saved = True Application.Quit Application.DisplayAlerts = True Application.EnableEvents = True End Sub This has made working with the code easier with one exception. I am not sure how to handle the Error handling. For example, part of my code checks to see if a file is a txt or csv file. If it is neither, I want it to go to Sub LogError(ErrorMsg As String). My intention is to make a single error handler that would be used by all the various modules and subroutines. Sub RT_SETUP_WKBTEMP() If File_Extension = "txt" Then Call RT_FORMAT_TXT_FILE ElseIf File_Extension = "csv" Then Call RT_FORMAT_CSV_FILE Else Call LogError("Not a .csv or .txt file") End If 'Do some more stuff..... End Sub However, I see that because Sub RT_SETUP_WKBTEMP() "Calls" the error handler subroutine, it comes back to Sub RT_SETUP_WKBTEMP() where it left off after running through the error handler subroutine. I don't want this. I want the program to end after running through the error handler subroutine.
As you already indicated in your comment, you can raise a custom error and handle it with an error handler. You can also use On Error... to log the entire call stack. Once an error occurred, the On Error... in the current procedure no longer applies, and error handling is passed to the calling procedure, from which you can write another error log entry: Public Path As String Public wkbTemp As Workbook Public StartTime As Double Public CurrentDateTime As Date Etc.... Sub RT_CMM_DATA_COMPILER_MAIN() Do some stuff.... On Error GoTo ErrHandlerMain Call RT_SETUP_WKBTEMP Call RT_COMPILE_TABLE_HEADER Call RT_RUN_LOG Call RT_SAVE_WORKBOOKS Do some stuff.... ErrorExit: 'Clean up and exit On Error Resume Next Application.Quit Application.DisplayAlerts = True Application.EnableEvents = True Exit Sub ErrHandlerMain: Call LogError("Error in Sub RT_CMM_DATA_COMPILER_MAIN") Resume ErrorExit 'Go to the clean-up code and Exit Sub End Sub Sub RT_SETUP_WKBTEMP() Const iCUSTOM_ERROR As Integer = 513 On Error GoTo ErrHandlerSetup If File_Extension = "txt" Then Call RT_FORMAT_TXT_FILE ElseIf File_Extension = "csv" Then Call RT_FORMAT_CSV_FILE Else 'This error will be handled by ErrHandlerSetup Err.Raise vbObjectError + iCUSTOM_ERROR End If 'Do some more stuff..... Exit Sub ErrHandlerSetup: Call LogError("Error in Sub RT_SETUP_WKBTEMP: Not a .csv or .txt file") 'Raise another error, which will be handled by ErrHandlerMain in Sub RT_CMM_DATA_COMPILER_MAIN Err.Raise vbObjectError + iCUSTOM_ERROR End Sub Sub LogError(ErrorMsg As String) 'Modified to not include any clean-up code wkbErrorLog.Activate On Error Resume Next Range("A" & LRow).Value = CurrentData_FilePath Range("B" & LRow).Value = CurrentDateTime Range("C" & LRow).Value = ErrorMsg Etc.... wkbErrorLog.Save wkbErrorLog.Saved = True End Sub The error log in this case will look something like the following: Error in Sub RT_SETUP_WKBTEMP: Not a .csv or .txt file Error in Sub RT_CMM_DATA_COMPILER_MAIN
The second of 2 'On Error goto ' statements gets ignored
I have some code that tries to set 11x17 paper as a default... On Error GoTo PageSizeErr ActiveSheet.PageSetup.PaperSize = xlPaperTabloid ' more code here PageSizeErr: On Error GoTo PageErr2 ActiveSheet.PageSetup.PaperSize = xlPaper11x17 'try another 11x17 driver definition GoTo resumePrinting PageErr2: MsgBox ("There's a problem setting Tabloid paper for the printer you have selected." & Chr(10) _ & "If you have an 11x17 printer selected, please contact EMBC, otherwise, try a different printer.") Exit Sub -------------- end of code sample ----------------- When it gets to the second 'ActivateSheet.PageSetup... line, instead of going to PageErr2 lable I get an error dialog box. (I have a printer selected that doesn't support 11x17 which is what I'm trying to test for.) The multiple error handlers are needed as it seems that different printer drivers handle the setting the differently. Why doesn't the second 'On Error goto ' statement get recognized?
You can't use on error goto within an error handler. See http://www.cpearson.com/excel/errorhandling.htm Maybe try something like this: Sub Tester() Dim pSize As XlPaperSize pSize = xlPaperTabloid On Error GoTo haveError: ActiveSheet.PageSetup.PaperSize = pSize 'print stuff... Exit Sub haveveError: If pSize = xlPaperTabloid Then pSize = xlPaper11x17 Resume End If MsgBox ("Couldn't print using tabloid or 11x17") End Sub
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