Since VBScript has notoriously bad built-in error handling, I've tried getting around that by wrapping my main blocks in a Do While Err.Number = 0 loop. Theoretically, this should work: if a script has On Error Resume Next statement enabled, the script will bypass the built-in error handler in WSH (i.e. stop it completely) while still populating its Err object. What should happen next is that the error number changes, which should trigger an interruption in the loop and have it jump out to my other branch of code. (Sorry for the ASM references.)
However, this doesn't seem to work all of the time. Sometimes, it will do exactly what's described above and work beautifully. Other times, though, it just completely ignores the change and keeps going. Even when I check the object in a debugger, I can see WSH changing the error number and its other members but blindly ignoring the loop condition!
What gives?
Example:
Dim TestObject
Do While Err.Number = 0
Set TestObject = CreateObject("Scripting.FileSystem") 'should trigger an error
WScript.Echo "Who cares about your DO?!"
Loop
WScript.Echo "Script should go here."
WScript.Quit(0)
Thanks!
Actually, never mind. VBscript isn't like compiled languages and can't branch out of loops because it executes every statement procedurally. Yet another reason to switch my scripts to .NET or something.
Related
I'm lead to believe that the vba IDE doesn't allow proper breaking on errors. I asked a question on how to identify runtime errors here:
How do I break on errors?
The solution / workaround seems to be to use On Error GoTo ErrorHandler or similar. I'm trying to make that work but haven't had much success.
According to Microsoft On Error GoTo will send you to the specified code of region when a runtime error occurs (https://msdn.microsoft.com/en-us/library/5hsw66as.aspx). Based on my experience, that's not the whole story. This question is about how to actually make that solution work.
So I have the following function:
Function generateTimeseries() As Variant
On Error GoTo ErrorHandler
Dim currentArray As Range
currentArray = Selection.Range ' this doesn't work - I don't care why
generateTimeseries = currentArray.Rows
Return
ErrorHandler:
Debug.Assert False
Resume ' break here if you want to catch errors
End Function
This function never enters the ErrorHandler code. Instead, it falls over on Selection.Range, with the function simply returning #VALUE. I don't actually care why this breaks, I just want to know how I can get the IDE to tell me that it actually fell over on that line, without me jumping through the code manually.
I just tested and your error handler works, it breaks correctly on
Debug.Assert False
--> check the debugger options.
However this is not a correct way to handle errors in vBA, because if you compile your application and forget to adapt such error handler, when a user will encounter an error in the procedure, the application will fall into an infinite loop.
In mostly all situations, you have to throw a msgbox to let the user know that an error has occurred, and terminate the current procedure. The only situation where this is not applicable is when you do something that can trigger an error but you know it and deliberately want to bypass the error. It's a rare situation.
I have never used Assert method and this is how I handle my errors in every procedure
Function generateTimeseries() As Variant
On Error GoTo ErrorHandler
Dim currentArray As Range
currentArray = Selection.Range ' this doesn't work - I don't care why
generateTimeseries = currentArray.Rows
Exit_Function:
Exit Function
ErrorHandler:
MsgBox Err.Description, vbCritical, "Error " & Err.Number
Resume Exit_Function ' breakpoint here if you want examine the code after error, otherwhise it terminates the function
Resume ' Once the code breaks on the above line, move to this instruction and F8 to return to the statement in error
End Function
I had enabled the feature to "Break on all Errors" in an earlier attempt to break on all errors.
I turned this off by going to Tools->Options->General and replacing Break on All Errors with Break on Unhandled Errors.
It turns out that Break on All Errors is Windows for "Break on Some Errors, but just return garbage if it's a UDF error".
How can I capture the variable name or object name causing an error in VBA?
Ex
Sub test()
On Error GoTo Handler
i = 0
n = 1/i
Handler:
Select Case variablename
Case "n"
'do something................
The error is not being caused by a variable. The error is caused by the line of code that attempts to divide 1 by 0. The assignment doesn't actually happen.
The best way I've found to diagnose issues when I'm actively developing is to have Stop and then Resume in the error handler.
Public Sub func()
On Error GoTo ErrHandler
'... some code here ...
Exit Sub
ErrHandler:
Stop
Resume
End Sub
The way this works is, when there is an error VBA will break (pause the execution, show the instruction pointer arrow and yellow highlighting) on the Stop. You can step to the Resume and then once more to find out which specific line of code is causing the error.
Once you start to understand what errors you are encountering you can build individual If ... Then cases for them to handle each appropriately. When you are ready to release your code into a production environment (i.e., for other users), you would need to replace the Stop/Resume with an unexpected error handler that either logs it or displays it to the user.
While Blackhawk's answer works fine for a small example, I personally prefer to avoid modifying code to track down errors like that. If I'm working on a lot of files trying to track down a nasty bug, it's entirely possible that I might forget to remove any "special code" like Stop statements, etc. after I've finished debugging. If any such code makes it to production, that can be quite embarrassing for me. In addition, it can be annoying having to add this code to many different places in order to figure out where the error is happening in the first place (it's not always as obvious as in your example!).
For these reasons, I prefer to use the facilities provided by the VBA Editor. So:
Open the VBA Editor.
Go to Tools > Options.
Select the General tab and click the Break on All Errors radio button.
Click OK.
Now run your code and you'll notice that when it reaches this line:
n = 1/i
you'll get an error dialog saying what the problem is ("Run-time error '11': Division by zero") and you'll get the option to click the "Debug" button on the dialog, which will break execution at the above line and will highlight it in yellow.
Once you're done with your debugging and find the error that had been troubling you, you can go back to the VBA Editor options and revert the "Error Trapping" setting back to the default option ("Break on Unhandled Errors").
This way, you've done no changes to the code that you need to remember to revert before releasing the code and the actual operation to set/unset this behaviour only takes a few clicks.
The Problem
I am trying to debug some code, and somewhere in the middle I stopped at a breakpoint. Now I want to change some variables and run a certain loop several times.
How far did I get?
I know how to change the variables, but somehow I get stuck when trying to run the loop in the immediate window. Here is an example:
Dim i As Integer
Dim j As Integer
For i = 0 To 6
j=i ' Do something
Next i
I tried several variations of the code, but each time I get the following error:
Compile error: Next without for
Other relevant information
I tried searching but mostly found information about problems with loops, whilst I am quite sure the loop itself is fine. (Especially as I reached it before arriving at the breakpoint).
The only place I saw someone addres this situation, he reduced the loop to a single line, however to do this every time would be very impractical in my case.
I realize that I could call a function containing the loop, and then the function call would probably work, but again this feels quite impractical. So I guess it boils down to the following question.
The question
What is a practical way to run a loop whilst debugging VBA code in Excel?
There is actually a way for using loops or other multi-line statements in the Immediate Window - using a colon : to separate statements instead of a new line.
Full solution is described here.
Note that in the Immediate Window you also don't have to declare the variables using a Dim statement.
To summarize, your snippet would look something like this:
For i = 0 To 6: j=i: debug.Print i+j: Next i
I think I understand your question. You want to run a multi-line code block (i.e. the loop) in the Immediate Window. This throws errors because the Immediate Window is only intended for single lines of code.
I don't have any suggestions other than those you already mentioned. I'd recommend putting your test loop into a separate function and calling that from the Immediate Window:
Sub Test()
Dim i As Integer
Dim j As Integer
For i = 0 To 6
j=i ' Do something
Next i
End
Another option is to set several breakpoints. You can also run one line of code at a time with F8.
What is likely the preferred method (i.e., what most people actually do) is use the full power of the IDE, which includes the Immediate, Locals and Watch panes. You can change the value of most variables at runtime by direct assignment in the Immediate Pane (i=6 will do exactly what you think it should do). The IDE also allows you to set breakpoints, add watch conditions, step through code line-by-line using the F8, step through function or procedure calls using Shift+F8, stepping over (and back) through code using the mouse/cursor, and with a few exceptions, you can even add new variables during runtime.
Has anyone ever run into breakpoints in the VBA console actually introducing errors? How could this possibly happen?
The short version of the story is that I have some code that works as expected when executed with no breakpoints toggled on. However, when breakpoints were toggled on in a few locations (in a subroutine and in a class method), the code would throw inexplicable errors when stepping through the code. I would ordinarily find this an extremely unlikely explanation, but after spending 2.5 days trying to debug the code only to discover that it executes properly with breakpoints turned off, I'm having trouble understanding what could have been the problem.
Details:
Two different errors occur when breakpoints are on.
Run-time error '457': This key is already associated with an element of this collection
and
Run-time error '424': Object required
The 457 error would occur after stepping through the code from the breakpoint to the line
dictE.Add M.SubMatches.Item(0), tmpQ
where one of the conditions for reaching this line of code was that dictE.Exists(M.SubMatches.Item(0)) was False.
The 424 error would occur after choosing Debug when the 457 error occurred. In a similarly zany manner, the error would be thrown by the (different) line
pathE = dictE.Item(Matches.Item(i).SubMatches.Item(0)).InFile
where one of the conditions for reaching this line of code was that dictE.Exists(Matches.Item(i).SubMatches.Item(0)) was True (and the .InFile property of the object is always defined).
Both of these errors occur in the same method of a custom class. I'd like to post all of the relevant code, but it's work-related and proprietary. Any ideas why this would happen? Is this a known issue? I tried googling for reports of similar problems, but I found nothing.
Not sure if it relates to your issue, but here's an illustration of how unexpected things can happen while using a dictionary and halted on a breakpoint...
Place a breakpoint on Debug.Print "Break" and then run this code:
Sub Tester()
Dim d As Object
Set d = CreateObject("scripting.dictionary")
d.Add "K1", "first"
d.Add "K2", "second"
d.Add "K3", "third"
Debug.Print d.Count '>>3 - as expected
Debug.Print "Break" ' Enter '? d("blah")' in Immediate pane
' while halted here...
Debug.Print d.Count '>> 4 ! key "blah" was added...
End Sub
I haven't read much about it, but the author at the link below recommends that I don't use "bubble up" to centralize error handling in VBA.
Excel Programming Weekend Crash Course via Google Books
But I'm not sure why he recommends that, and he doesn't really explain.
Can someone tell me why I should put error handling in EVERY procedure instead of using "bubble up"? Or at least, do you know why the author says not to?
Thanks.
The short answer to your first question is "you shouldn't put an error handler in EVERY procedure". But usually some routines do need them!
To say that "every procedure must have an error handler" is in general terrible advice. The flaws with VBA error handling have been much discussed elsewhere. Conceptually, though, it's not all that different from the more standard form of exception handling found in other languages. Most of the best practices from those languages apply. You should handle errors at the lowest level where handling them makes sense. Sometimes this is in the procedure where the error occurred, many times not.
Often the most meaningful thing an internal routine can do when an error occurs is just let it pass on up the stack so it can reach code that knows what to do with it. It really depends on the routine and how it fits with the rest of the program.
Consider these examples:
A handler in a calling procedure will handle all errors raised by the routines it calls. So if a particular routine doesn't need any cleanup, then don't put any error-handler code there:
Sub Caller()
On Error GoTo HANDLER
ChildProc
On Error GoTo 0
Exit Sub
HANDLER:
Debug.Print Error, "Parent cleanup - something happened in either this procedure or a procedure that it called"
End Sub
Sub ChildProc()
Debug.Print 10 / 0 ' causes error
'Don't bother handling errors here since there's nothing this routine can do about them
End Sub
On the other hand, you may need cleanup tasks, in which case you need an error handler.
Sub Caller()
On Error GoTo HANDLER
ChildProc
On Error GoTo 0
Exit Sub
HANDLER:
Debug.Print Error, "Parent cleanup"
End Sub
Sub ChildProc()
'Pretend this routine gets ahold of some resource that must be cleaned up when it's done
call get_resources()
On Error GoTo HANDLER
Debug.Print 10 / 0 ' causes error
On Error GoTo 0
'Clean up once we're done
call release_resources()
Exit Sub
HANDLER:
Debug.Print Error, "Child cleanup"
'Clean up in case of an error
call release_resources()
'Raise another error if necessary to let callers know something went wrong
Err.Raise 10000, "ChildProc", Error
End Sub
The above examples are just meant to illustrate the point about why you might need or not need an error handler in a given routine. So it's worth noting that in real code the "resource" example is usually handled better with an RAII technique where the error handling is encapsulated with the resource acquisition and release - see https://stackoverflow.com/a/3792280/58845 for a VBA example. And things like whether to re-raise a caught error are also situation-dependent. Sometimes it's possible to handle the error entirely locally and then there is no need to tell callers that anything went wrong.
The answer to your second question is that the author doesn't seem to understand exception handling very well. He admits that error handling is context specific, but then seems to suggest that every procedure should locally decide between "correct the problem right here and resume execution" and "terminate the program". He leaves out the usually correct option, which is "clean up locally and kick the problem upstairs". So routines with no need to clean up locally should just let errors "bubble up".
My 2 cents:
You should put error handlers on all Public Procedures and Events. This means that the procedure at the bottom of the call stack will always have an error handler. Then add error handlers in your other procedures as it makes sense. If an error occurs in a procedure that does not have an error handler, it will "bubble up" to the top level error handler where it be logged/displayed in a professional fashion.
A scenario where you might want to add an error handler to a private (lower level) procedure is this:
The code needs to be fast. You have a rare condition that can be avoided, but will force you to perform an expensive logical test inside of a loop (or worse a nested loop).
You might perform the logical test in the error handler, and if it's said "rare occurrence" make the correction and resume. As the condition is rare, you will see performance gains for most conditions. If the error handler can't figure out and correct the problem then re-raise the error to bubble it on up the stack.
Obviously this is just one scenario.
I'm not sure what the default error handling of VBA is, but since its Visual Basic for Applications, and those applications include things like excel and word, I assume just a dialog box will appear which will not be helpful to the user.
I assume that the author has been bitten by code not handling errors so he now recommends all procedures to handle errors.
The full answer is that you have to be aware of every error that can occur and to have code in place to handle it, whether it is as low as possible (where you may not know what to do), or as high as possible (which means less effort writing error handling code, but not knowing why the error occurred), or strategically (which is just in the right places where you should be able to recover from most common errors) or just everywhere (which may be just too much development effort).
I see at least one reason in his explanation: because doing so deprives you from the benefit of Resume (next).
Plus you won't know in which module the error happened.
It is better not to use "bubble-up" part of error handling because errors should be handled & if it is known as to what to do, if such an error occurs - is better known to procedure as to what to do than the calling procedure.
Sub test()
On Error GoTo e
Dim c As Integer
Dim d As Integer
c = add(5, 0)
d = divideWhichManagedItsOwnErrorHandling(5, 0)
d = divide(5, 0)
Exit Sub
e:
MsgBox "error occurred somewhere for which I don't know what to do: " + Err.Description
End Sub
Function add(a As Integer, b As Integer) As Integer
add = a + b
End Function
Function divide(a As Integer, b As Integer) As Integer
divide = a / b 'if error occurs, it will "bubble-up" to the caller.
End Function
Function divideWhichManagedItsOwnErrorHandling(a As Integer, b As Integer) As Integer
On Error Resume Next
Dim result As Integer
result = a / b
If Err.Number = 11 Then 'if divide by zero occurred, user must have passed 0 for b
result = 0 ' return 0 if the divide by zero occurs.
End If
divideWhichManagedItsOwnErrorHandling = result
End Function