Resume to a label not working as expected - vba

I am having an issue using Resume and although I have found another solution by using an On Error GoTo, I am still confused as to why the code below doesn't work. The initial error occurs because the sheet name "Sheet_1" is already taken. This means that, in the watch window, err.number has a value of 1004 just before the Resume NameAgain is executed. Rather than clear the error and jump back up to the label, an error 20 occurs(resume without error), and the code moves to the End If line.
Given that there is an active error 1004, I can't understand why it acts as though there isn't an error. I have searched the site for Error 20 issues but nothing really resolved this for me or made me understand the logic behind it. Any help is much appreciated.
Sub ErrorTest()
Dim i as integer:i=1
NameAgain: On Error Resume Next
Worksheets("Main").Name = "Sheet_" & i
If Err.Number = 1004 Then
i = i + 1
Resume NameAgain
End If
End Sub
Update after paxdiablo comment:
The above was a poor attempt at trying to replicate but simplify a problem I was having. The section of code I am working with is below:
Activate CheckBook to use ActiveWindow
CheckBook.Activate
Set DestSheet = CheckBook.Worksheets.Add(After:=CheckBook.Sheets(1))
On Error Resume Next
v = 1
NameAgain: DestSheet.Name = ExpBookName & "_" & v
If Err.Number = 1004 Then
v = v + 1
Resume NameAgain
End If
On Error GoTo 0
ActiveWindow.DisplayGridlines = False
Set DestCell = DestSheet.Range("A2")
So the solution is to move the On Error Resume Next to the label line and use GoTo in place of Resume.

The resume statement is a means to, from within an error handler, go back to some point in your main (non-error-handling) code and resume execution.
In this case, you've explicitly stated you want to automatically resume next in the event of an error.
This is functionally equivalent to (VB-like pseudo-code):
line:
on error goto handler
cause error
resume line ' not in an error handler at this point '
handler:
resume next
So you're not technically in an error handler at the point where you try to resume to the label.
The right statement for what you're trying to do would be a simple goto rather than resume.

A more correct solution is to write code that does not deliberately generate errors or which does not use Goto.
Public Function GetNextSheetName(ByVal ipWb As Excel.Workbook, ByVal ipStemName As String) As String
Dim mySheet As Variant
Dim mySD As Scripting.Dictionary
Set mySD = New Scripting.Dictionary
For Each mySheet In ipWb.Sheets
' mySd.Count is a dummy entry to satisfy
' the Key and Item requirements for .Add.
' we are only interested in the Keys
' for use with the .Exists method later
mySD.Add mySheet.Name, mySD.Count
Next
Do
DoEvents
Dim myIndex As Long
myIndex = myIndex + 1
Dim myNextSheetName As String
myNextSheetName = ipStemName + "_" & CStr(myIndex)
Loop While mySD.Exists(myNextSheetName)
GetNextSheetName = myNextSheetName
End Function
Which now allows
Set DestSheet = checkbook.Worksheets.Add(After:=checkbook.Sheets(1))
DestSheet.Name = GetNextSheetName(checkbook, ExpBookName)
ActiveWindow.DisplayGridlines = False
Set DestCell = DestSheet.Range("A2")

Related

Error Handling within a Loop for PivotTable object sorting

I've been trying to sort PivotTable objects in VBA using a function I've attempted:
Public Function PTSort(PTName As String, PTFieldName as String, SortArray As Variant)
Dim m as Integer: m = 1 'Row counter
Dim i as Long 'Dummy Variable for cycling
With ActiveSheet.PivotTables(PTName).PivotFields(PTFieldName)
.Parent.ManualUpdate = True
For i = LBound(SortArray) To UBound(SortArray)
With .PivotItems(SortArray(m - 1)) 'For in-code array
.Position = m
End With
m = m + 1
Next i
.Parent.ManualUpdate = False
End With
End Function
Whilst this works well with a known set of elements within SortArray, I have a master list to follow whilst sorting (so as to standardise a few orders accross several PivotTables), in which the PivotTable need not necessarily contain all said PivotItems. I have hence improved it to the following:
Sub PTSort(PTName As String, PTFieldName as String, SortArray As Variant)
Dim m as Integer: m = 1
Dim i as Long
Dim k As Integer: k = 1 'To cycle the position independently from the array in the event of disjoint.
With ActiveSheet.PivotTables(PTName).PivotFields(PTFieldName)
.Parent.ManualUpdate = True
For i = LBound(SortArray) To UBound(SortArray)
On Error GoTo ERRHANDLER:
With .PivotItems(SortArray(k)) 'After parsing from range of cells into VariantArray, then does one not require the "-1"
.Position = m
End With
m = m + 1
ExitHandler:
k = k + 1
Next i
.Parent.ManualUpdate = False
End With
GoTo ENDEND:
ERRHANDLER:
GoTo EXITHANDLER:
ENDEND:
End Sub
The OnError GoTo seems to only work once though, irregardless of how high up placed it?
Help would be greatly appreciated. Thanks in advance!
This is from MSDN on Visual Studio but I think it applies to VBA in the same way.
An "enabled" error handler is one that is turned on by an On Error statement. An "active" error handler is an enabled handler that is in the process of handling an error.
If an error occurs while an error handler is active (between the occurrence of the error and a Resume, Exit Sub, Exit Function, or Exit Property statement), the current procedure's error handler cannot handle the error. Control returns to the calling procedure
So after your code first gets to On Error GoTo ERRHANDLER, ERRHANDLER is enabled. Then when an error occurs ERRHANDLER is activated and handles the error. When you GoTo EXITHANDLER it stays active and still handles the error. The On Error GoTo ERRHANDLER has no effect at this point.
To re-enable the ERRHANDLER you need to use Resume EXITHANDLER instead of GoTo EXITHANDLER.
Edit on the Resume statement. There are three ways to use Resume: Resume, Resume Next and Resume label
Resume without an argument causes the code to resume at the line that caused the error. This obviously has to be used very careful as you must be absolutely certain that you fixed the problem or you'll end up in an infinite loop.
Resume Next causes the code to resume at the line after the line that caused the error.
Resume Label is almost the same as GoTo Label but with this you exit the error handler and resume normal code execution. The error handler is re-enabled.

Excel VBA error handling not working for second error

In the below code Errorhandler correctly takes care of first error when I enter a workbook that is not open or any random string. But when I click on retry and again enter a random name I get "Subscript out of Range" error # Workbooks(workbookname). Activate.
Can anyone help me why it is happening and how can I make it work. I have tried a lot of things. But nothing is working. This code is part of a larger program.
Sub test()
Dim workbkname As String
On Error GoTo Errorhandler
Retry:
workbookname = InputBox("Enter workbook name:", _
"Name Enrty")
If StrPtr(workbookname) = 0 Then
MsgBox ("Aborting Program")
End
End If
Workbooks(workbookname).Activate
Exit Sub
Errorhandler:
Response = MsgBox("Workbook " & workbookname & " not found", vbRetryCancel)
If Response = 4 Then
GoTo Retry
End If
End Sub
The issue here is that the VBA Error Handler does not clear the error once given a directive like GoTo. As a result, the code thinks that it has encountered an error within your error handling routine and thus throws the error up the stack.
In order to clear the error, you must either call Resume with a place to resume (either Resume alone to run the erroneous code again, Resume Next to resume the line of code following the error, or Resume can be called followed by a label, as below:
Sub ErrTest
On Error Goto ErrHndl
For i = 0 to 5
a = 1/i
nextiLabel:
Next i
Exit Sub
ErrHndl:
Resume nextiLabel
End Sub
You can also use Err.Clear() to remove the error, but that is less recommended.
See here for more info: Error Handling in Excel

How to check the availability of a worksheet

I have to run a set of code related to worksheet "wins", but only if that worksheet exist.
Please share a code to check the availability of sheet "wins". If worksheet "wins" exist, then only I want to run that set of code, else I want to skip executing that set of code and move to next line of code.
You could use On Error Resume Next to skip the errror which occurs if you try access a not existing worksheet and assigning it to a object variable. So if the worksheet does not exist, no error occurs but the variable is Nothing. If the worksheet exists, then the variable is not Nothing.
Example:
Sub test()
Dim wsWins As Worksheet
On Error Resume Next
Set wsWins = ActiveWorkbook.Worksheets("wins")
On Error GoTo 0
If Not wsWins Is Nothing Then
MsgBox "Worksheet wins exists."
Else
MsgBox "Worksheet wins does not exist."
End If
End Sub
Axel's answer will work nicely. Some people prefer not to use error throwing to test if something exists. If you're one of them then I use the following quite a lot in a Utility module. It'll work for Worksheets, Charts, etc. (basically anything that's a collection with a 'Name' property):
Public Function ExcelObjectExists(testName As String, excelCollection As Object) As Boolean
Dim item As Object
On Error GoTo InvalidObject
For Each item In excelCollection
If item.Name = testName Then
ExcelObjectExists = True
Exit Function
End If
Next
ExcelObjectExists = False
Exit Function
InvalidObject:
MsgBox "Developer error: invalid collection object passed in ExcelObjectExists."
ExcelObjectExists = False
End Function
You can call it like this:
If ExcelObjectExists("wins", ThisWorkbook.Worksheets) Then

On Error works in first and doesn't work in second instance. Bug?

I have a very strange problem here. Here's the code:
reqLang = "ENG"
Select Case reqLang
Case "CRO", "ENG"
'first loop -------------------------------------
On Error GoTo reqLangVisible
i = 1
'Loop until ccCROENG's are all hidden and then go to reqLangVisible.
Do
ActiveDocument.SelectContentControlsByTag("ccCROENG")(i) _
.Range.Font.Hidden = True 'hides all CCs
i = i + 1
Loop
reqLangVisible:
'second loop -------------------------------------
On Error GoTo langOut
i = 1
'Loop until ccreqLang's are all visible and then go to langOut.
Do
ActiveDocument.SelectContentControlsByTitle("cc" & reqLang)(i) _
.Range.Font.Hidden = False 'activates reqLang CCs
i = i + 1
Loop ' CAN'T GET OUT -----------------------------------
Case "CROENG"
i = 1
'Loop until ccCROENG's are all visible and then go to langOut.
Do
On Error GoTo langOut
ActiveDocument.SelectContentControlsByTag("ccCROENG")(i) _
.Range.Font.Hidden = False 'Shows all CCs
i = i + 1
Loop
End Select
langOut:
MsgBox "Success!" '------- yeah, not really.
Stop
I hope it's clear enough what it's trying to do (at least programming-wise). I have multiple ContentControls(CCs) with same titles and tags. The problem I end up with is marked with CAN'T GET OUT, because, you guessed it - I can't get of this second loop! I end up with the Out of range error because it runs out of CCs.
What's even weirder is that it actually did get out of the first loop which has the exact same On Error statement, thought pointing to a different section.
Is it me, or did I just - however unlikely - run onto a bug in VBA?
In any case, is there a solution or at least a workaround?
Typically you only use error handling for dealing with unexpected or unpredictable situations, such as not being able to access a drive, or finding you have no network access.
Error handling is not intended as a substitute for reasonable checks which could otherwise be done. i.e. collections have a Count property which you can use when looping over their items, so avoiding any error caused by trying to access Item(n+1) when there are only n items (and here you know n from Count). Alternatively, use a For Each loop.
Here's some sample code demonstrating use of two methods for looping over your controls:
Sub Tester()
Dim cc1 As ContentControls, cc2 As ContentControls
Dim c, i As Long
With ActiveDocument
Set cc1 = .SelectContentControlsByTag("tbTag")
Set cc2 = .SelectContentControlsByTitle("tbTitle")
End With
Debug.Print "cc1 has " & cc1.Count
Debug.Print "cc2 has " & cc2.Count
'use the Count property
For i = 1 To cc1.Count
Set c = cc1(i)
c.Range.Font.Hidden = True
Next i
'use a For Each loop
For Each c In cc2
c.Range.Font.Hidden = False
Next c
End Sub
This is the type of scenario for which this type of flow control is designed.
Applied to your original code:
Sub Tester2()
Dim reqLang, cc As ContentControls, c
reqLang = "ENG"
Select Case reqLang
Case "CRO", "ENG"
Set cc = ActiveDocument.SelectContentControlsByTag("ccCROENG")
SetTextHidden cc, True
Set cc = ActiveDocument.SelectContentControlsByTitle("cc" & reqLang)
SetTextHidden cc, False
Case "CROENG"
Set cc = ActiveDocument.SelectContentControlsByTag("ccCROENG")
SetTextHidden cc, False
End Select
MsgBox "Success!" '-- yeah really
End Sub
Sub SetTextHidden(cc As ContentControls, MakeHidden As Boolean)
Dim c
For Each c In cc
c.Range.Font.Hidden = MakeHidden
Next c
End Sub
So if you've read my comment, and to formally answer your question, it is not a bug.
You just need to use Error Handling Routines correctly.
What you're trying to do is somewhat like below. HTH.
Select Case reqlang
Case "CRO", "ENG"
On Error Resume Next '~~> ignores the error when encountered
'~~> Your loop which possibly creates the error goes here
On Error Goto 0 '~~> resets the actively triggered error handling
Case "CROENG"
On Error Resume Next '~~> ignores the error when encountered
'~~> Your loop which possibly creates the error goes here
On Error Goto 0 '~~> resets the actively triggered error handling
End Select
MsgBox "Success"
But as the link suggest, you need to handle errors and not simply disregard them. Try cheking on the actual error and find a way to correct it or avoid it.
You'll be surprise that you won't even be needing the Error Handling Routine.

Excel VBA: On Error Goto statement not working inside For-Loop

I'm trying to cycle through a table in excel. The first three columns of this table have text headings, the rest of them have dates as headings. I want to assign those dates, sequentially, to a Date-type variable, and then perform some operations based on the date
To do this I am using a foreach loop on myTable.ListColumns. Since the first three columns do not have date headers, I have tried to set the loop up so that, if there is an error assigning the header string to the date-type variable, the loop goes straight to the next column
This seems to work for the first column. However, when the second column's header is 'assigned' to the date-type variable, the macro encounters an error even though it is within an error-handling block
Dim myCol As ListColumn
For Each myCol In myTable.ListColumns
On Error GoTo NextCol
Dim myDate As Date
myDate = CDate(myCol.Name)
On Error GoTo 0
'MORE CODE HERE
NextCol:
On Error GoTo 0
Next myCol
To reiterate, the error is thrown on the second round of the loop, at the statement
myDate = CDate(myCol.Name)
Can anyone explain why the On Error statement stops working?
With the code as shown, you're actually still considered to be within the error handling routine when you strike the next statement.
That means that subsequent error handlers are not allowed until you resume from the current one.
A better architecture would be:
Dim myCol As ListColumn
For Each myCol In myTable.ListColumns
On Error GoTo ErrCol
Dim myDate As Date
myDate = CDate(myCol.Name)
On Error GoTo 0
' MORE CODE HERE '
NextCol:
Next myCol
Exit Sub ' or something '
ErrCol:
Resume NextCol
This clearly delineates error handling from regular code and ensures that the currently executing error handler finishes before you try to set up another handler.
This site has a good description of the problem:
Error Handling Blocks And On Error Goto
An error handling block, also called an error handler, is a section of code to which execution is tranferred via a On Error Goto <label>: statement. This code should be designed either to fix the problem and resume execution in the main code block or to terminate execution of the procedure. You can't use the On Error Goto <label>: statement merely skip over lines. For example, the following code will not work properly:
On Error GoTo Err1:
Debug.Print 1 / 0
' more code
Err1:
On Error GoTo Err2:
Debug.Print 1 / 0
' more code
Err2:
When the first error is raised, execution transfers to the line following Err1:. The error hander is still active when the second error occurs, and therefore the second error is not trapped by the On Error statement.
You need to add resume of some sorts in your error handling code to indicate the error handling is over. Otherwise, the first error handler is still active and you are never "resolved."
See http://www.cpearson.com/excel/errorhandling.htm (specifically the heading "Error Handling Blocks And On Error Goto" and following section)
Follow-up to paxdiablo's accepted answer. This is possible, allowing two error traps in the same sub, one after the other :
Public Sub test()
On Error GoTo Err1:
Debug.Print 1 / 0
' more code
Err1:
On Error GoTo -1 ' clears the active error handler
On Error GoTo Err2: ' .. so we can set up another
Debug.Print 1 / 0
' more code
Err2:
MsgBox "Got here safely"
End Sub
Using On Error GoTo -1 cancels the active error handler and allows another to be set up (and err.clear doesn't do this!). Whether this is a good idea or not is left as an exercise for the reader, but it works!
Clearing all property settings of the Err object is not the same as resetting the error handler.
Try this:
Sub TestErr()
Dim i As Integer
Dim x As Double
On Error GoTo NextLoop
For i = 1 To 2
10: x = i / 0
NextLoop:
If Err <> 0 Then
Err.Clear
Debug.Print "Cleared i=" & i
End If
Next
End Sub
You'll notice the just like the OP, it will catch the error properly when i =1 but it will fail on line 10 when i = 2, even though we used Err.Clear
Dim ws As worksheets
For Each myCol In myTable.ListColumns
On Error GoTo endbit
Dim myDate As Date
myDate = CDate(myCol.Name)
On Error GoTo 0
'MORE CODE HERE
endbit:
Resume NextCol
NextCol:
' Next myCol
Exit Sub ' or something '