Debugging

31/07/2009 14:52

Debugging Your Scripts

Scripts will seldom be perfect right away.
This page describes some (debugging) techniques that will help you avoid errors in VBScript, or to find and correct them.

  1. Never assume anything
  2. Always use Option Explicit and declare all variables
  3. (Temporarily) disable all On Error Resume Next lines
  4. Modularize your scripts with functions and subroutines
  5. Use descriptive names for variables, functions and subroutines
  6. Initialize variables
  7. Avoid nested functions
  8. Display or log intermediate results like variable values and return codes
  9. Create and use a debug window
  10. Use a VBScript aware editor or IDE with built-in debugger
  11. Document your scripts with useful comments
  12. Use custom error handling
  13. Clean up

 

Never assume anything

This may be the most important thing to keep in mind when scripting -- in any language.

  • Never assume a WSH version.
    Check it using WScript.Version!
  • Never assume a Windows version.
    Check it!
  • Never assume .NET Framework is installed.
    Check it!
  • Never assume a script runs with administrative rights.
    Check it!
  • Never assume access to WMI is allowed.
    Use custom error handling to check it!
  • Never assume write access.
    Again, check it!
  • Never assume an open Internet connection.
    Check, check, check!
  • If you create a new instance of an object, use custom error handling with Err and IsObject to check if it was successfully created.
  • Always check if required extensions are available before trying to use them!
  • Well, you get the idea...

Use common sense.

Make sure you log any requirements that aren't met, and/or display descriptive error messages.

 

Always use Option Explicit and declare all variables

It may seem a nuisance to force yourself to declare all variables, but trust me, Option Explicit will save you a lot of time searching for errors caused by typos in variable names.
It will also help you find variables with the wrong scope (local vs. global).

 

(Temporarily) disable all On Error Resume Next lines

When you're looking for the cause of an error, you do want to see the error messages stating which line in the code generates the error.
So "comment out" any On Error Resume Next lines while testing/debugging.
And whenever you really do need to use On Error Resume Next, check for errors (If Err Then...), and switch back to On Error Goto 0 as soon as possible.

 

Modularize your scripts with functions and subroutines

Any block of code that will be used more than once can be moved into a subroutine or function.
Dividing the code into logical subroutines and functions will improve readability of your script, and thus will make maintenance a lot easier.
If a "self-contained" subroutine or function has been debugged, it will save debugging time when you reuse it in another script.

If your function or subroutine receives parameters, use distinctive parameter names to avoid conflicts with global variables. Do not use an existing variable name for a parameter name.
As you may have noticed, I use the prefix my for parameter names in my own scripts. Choose any naming system you want, but be consistent, and keep in mind that some day others may need to understand your code.

To be completely on the safe side, use ByVal as in

Function MyFunc( ByVal varParameter )

to prevent changing the value of an existing global variable named varParameter in the global scope.

Experiment with Denis St-Pierre's ByVal/ByRef test script to become familiar with the concepts.

 

Use descriptive names for variables, functions and subroutines

You are (almost) completely free to use any name for a variable, subroutine or function.
However, instead of using a name like f, why not use objFSO for a FileSystem object? Or Decode instead of dec as a function name?
Imagine what a difference it will make when someone else needs to read and understand your code (or you yourself a couple of months from now...).
By choosing logical, descriptive names, you may also save yourself time while debugging.

You may have noticed that many scripters use the first character, or the first 3 characters, of variable names to specify the data type: objFSO for a (FileSystem) object, intDaysPerWeek for integers, etc.
Though in VBScript any variable can contain any type of data at any time, this naming convention helps make clear what type of data a variable is supposed to contain.

For function or subroutine that receive parameters, use distinctive parameter names to avoid conflicts with global variables. Using existing variable names for parameter names spells trouble.
As you may have noticed, I use the prefix my for parameter names in my own scripts. You can choose any naming system you want. But do keep it consistent.
Keep in mind that some day others may need to understand your code.

Again, to be completely on the safe side, use ByVal as in

Function MyFunc( ByVal varParameter )

to prevent changing the value of an existing global variable named varParameter in the global scope.

I urge you to try Denis St-Pierre's ByVal/ByRef test script to build an understanding of the concepts.
It may save you days of stressful debugging.

 

Initialize variables

This may be important when you use loop counters other than For loops: make sure the counter variable has a valid value to start with.
Also watch out for global variables that are used in subroutines or functions.

 

Avoid nested functions

A one-liner like:

strFullPath   = "C:\Documents and Settings\Me\Application Data"
strParentName = Right( Left( strFullPath, InStrRev( strFullPath, "\" ) - 1 ), _
Len( Left( strFullPath, InStrRev( strFullPath, "\" ) - 1 ) ) - _
InStrRev( Left( strFullPath, InStrRev( strFullPath, "\" ) - 1 ), "\" ) )

is hard to debug if it returns the wrong result.
Split it up in several lines, each without nested functions, and use variables to contain the intermediate results:

strFullPath   = "C:\Documents and Settings\Me\Application Data"
intLastSlash = InStrRev( strFullPath, "\" )
strParentName = Left( strFullPath, intLastSlash - 1 )
intParentLen = Len( strParentName ) - InStrRev( strParentName, "\" )
strParentName = Right( strParentName, intParentLen )

Now, if the code doesn't do what it is supposed to do, you can have a look at the intermediate results to check where the problem lies.

 

Display or log intermediate results like variable values and return codes

To check the script's program flow, and the values of variables during execution, it helps to display variable names and their values during run time.
If external commands or objects are used, display their return codes as well.

Write every detail in a log file too, preferably with a timestamp in order to detect possible delays.
If subroutines or (user defined) functions are used, log each call to these subroutines, it will help you follow the program flow.

If I expect problems with a script, I often add an optional /DEBUG command line switch, which will tell the script to log even more details.

 

Create and use a debug window

This is a trick I learned from Don Jones, who describes it in his book VBScript, WMI, and ADSI Unleashed: Using VBScript, WMI, and ADSI to Automate Windows Administration.

Dim objIEDebugWindow

Debug "This is a great way to display intermediate results in a separate window."


Sub Debug( myText )
' Uncomment the next line to turn off debugging
' Exit Sub

If Not IsObject( objIEDebugWindow ) Then
Set objIEDebugWindow = CreateObject( "InternetExplorer.Application" )
objIEDebugWindow.Navigate "about:blank"
objIEDebugWindow.Visible = True
objIEDebugWindow.ToolBar = False
objIEDebugWindow.Width = 200
objIEDebugWindow.Height = 300
objIEDebugWindow.Left = 10
objIEDebugWindow.Top = 10
Do While objIEDebugWindow.Busy
WScript.Sleep 100
Loop
objIEDebugWindow.Document.Title = "IE Debug Window"
objIEDebugWindow.Document.Body.InnerHTML = _
"<b>" & Now & "</b></br>"
End If

objIEDebugWindow.Document.Body.InnerHTML = _
objIEDebugWindow.Document.Body.InnerHTML _
& myText & "<br>" & vbCrLf
End Sub
Notes: (1) objIEDebugWindow must be declared in the main script body, not in the subroutine (must be global)!
  (2) Do not discard the objIEDebugWindow object at the end of the script, or your debug window will vanish!

And this is what the debug window looks like:

Internet Explorer Debug Window

 

Use a VBScript aware editor or IDE

with built-in debugger and object browser

There are several VBScript aware editors (IDEs) available, some with built-in debugger. The main advantages of these editors are:

  • Different colors for commands and keywords: a typo will result in the wrong color
  • IntelliSense like "intelligence" and object browser: type a dot after an object name and you'll get a drop-down list of avaialble properties and methods
  • Built-in debugger: run the script "inside" the editor, add breakpoints, monitor variable values, get improved error handling

My personal favorite is VbsEdit, which saves me a lot of time and frustration when writing in VBScript.

Because there are more editors and more scripting languages, I compiled a list of script editors and IDEs.

 

Document your scripts with useful comments

It is always wise to add comments explaining what a script, or part of the script, does.
However, make sure the comments are relevant for future use -- scripts need maintenance every now and then, and comments can help make this easier.

If you intend to reuse code, like subroutines or user defined functions, it is essential to describe the functionality in comments.
Include a description of what the routine is intended for, its requirements, input parameters, output and/or return codes.

In short, describe it as a "black box": what goes in, what comes out, and how are the two related.

 

Use custom error handling

It is ok to use the scripting engine's built-in error handling, but adding your own custom error handling may result in a better "user experience".

Insert a line On Error Resume Next just before some code that might cause trouble.
Insert another block of code after that suspect code to deal with potential errors.
Use Err or Err.Number and Err.Description to detect and log and maybe even correct errors.
If no more problems are expected, insert a line On Error Goto 0 after the custom error handling code to restore the default built-in error handling.

On Error Resume Next

' some code that might raise an error

If Err Then
WScript.Echo "Error # " & Err.Number
WScript.Echo Err.Description
' take some action, or in this case, abort the script with return code 1
WScript.Quit 1
End If

 

Clean up

It is usually advisable to clean up any leftover objects at the end of the script.
Objects like the FileSystem object, Internet Explorer and others may cause memory leaks if they aren't discarded and new instances are being opened all the time.

Just make sure to add a Set objectName = Nothing line at each "exit" (just before each WScript.Quit) and end of the program flow.

Objects that are "created" inside a subroutine or function should always be discarded at the end of the routine.
A known exception to this rule is the Internet Explorer Debug Window discussed before.

 

 

Back