This month, I concentrate on a subject that often doesn't receive generic coverage: how you can trap and handle errors in VBScript code that you write for the Windows Script Host (WSH) environment. I discuss three practical ways you can trap and handle errors: explicitly declaring variables, basic error handling with the CheckError subprocedure, and advanced error handling with customized CheckError subprocedures.
Explicitly Declaring Variables
In VBScript, you don't have to explicitly declare the variables that you use. However, explicitly declaring variables is good practice. To make sure that you explicitly declare variables, you can use the Option Explicit command at the beginning of your script. Option Explicit forces the VBScript interpreter (in this case, WSH) to make sure that you use an explicit declaration (i.e., a Dim, Public, or Private statement) to declare a variable before you use it. If you explicitly declare variables and use Option Explicit, you'll spend less time debugging scripts because you'll immediately catch such errors as misspelled variable names and mismatched variables.
If your script has many variables and you put each explicit declaration on a separate line, your script can get long. To save space, you can take advantage of VBScript's vastly underused colon (:) to join two lines of code. For more information about this space saver, see the sidebar "Taking Advantage of VBScript's Colon," page 2.
Basic Error Handling
The CheckError subprocedure in Listing 1, page 2, lets you trap and handle runtime errors in any script. Here's how CheckError works. First, the subprocedure declares the strMessage variable. This variable will eventually contain a text string that provides information about an error if an error occurs. To determine whether an error has occurred, CheckError uses an If...Then...Else statement that draws on VBScript's Err object. (Because the Err object has a global scope, you don't have to instantiate this object before you use it.) The Err object's default property is Err::Number, so when the If...Then...Else statement checks whether Err = 0, it's actually checking whether Err.Number = 0. When no error occurs, Err.Number returns the value of 0; when an error occurs, Err.Number returns a nonzero integer that represents the type of error that occurred. So, if Err = 0, the subprocedure exits because no error has occurred. If Err is any other number, the subprocedure continues to the next line.
In the next line, CheckError uses the Err::Source, Err::Number (i.e., Err), and Err::Description properties to obtain data about the error and store that data in strMessage. Err::Source returns data about the error's source—that is, the class name or programmatic identifier (ProgID) of the object or application that caused the error. As I just explained, Err::Number returns data about the type of error that occurred. Err::Description returns a short description of the object or application that caused the error.
CheckError then prints the text string that strMessage contains. Finally, the subprocedure uses the WScript::Quit command to quit the script and returns an error code of 1 to indicate that an error occurred.
CheckError quits the script if an error occurs. If you want the script to continue executing after an error occurs, you can modify CheckError. Replace the line
Then when an error occurs, CheckError will print a message, reset the Err object to 0, and wait for another error while the script continues to execute.
CheckError isn't a script in and of itself. Thus, to use it, you need to perform three steps:
- Add the subprocedure in Listing 1 to the end of the script in which you want to check for errors.
- At the beginning of the script, add the line
- At the point in which you want to use CheckError in your script, add the line
CheckErrorto invoke the subprocedure. As callout A in Listing 2 shows, instead of specifying CheckError on a separate line, you can use a colon to append this command to the applicable line.
On Error Resume NextAs Listing 2 shows, you can place this statement after the Option Explicit command. The On Error Resume Next statement enables WSH to trap errors with the Err object. On Error Resume Next prompts WSH to reset the Err object's properties to 0 or zero-length strings ("").
Advanced Error Handling
You can adapt the CheckError subprocedure to extend its usability and capability. For example, to extend CheckError's usability, you can add code that checks the Err::Source value and, based on that value, sets different WScript::Quit error codes, such as 1 for a VBScript runtime error, 2 for a strobj.dll error, and 3 for an unknown error. With this customization, you can learn an error's source at a glance. To learn the type of error at a glance, you can capture the Err::Number value, then replace that number with a text string. With this customization, you don't have to remember or look up the type of error each number represents.
To extend CheckError's capabilities, you can make modifications based on the type of script you're running. If you're running a Windows Installer script or a Microsoft Active Directory Service Interfaces (ADSI) script, you can customize CheckError so that it traps and handles technology-specific types of errors.
Let me give you an example based on the script AddBackupPathForOneProduct.vbs from my April 2001 column "Scripting Solutions with WSH and COM: Customizing Windows Installer Applications." Although you can follow the three steps I just discussed to add the CheckError subprocedure to this script, a more comprehensive approach is to adapt both CheckError and AddBackupPathForOneProduct.vbs to create a script that includes a Windows Installer-savvy error handler. Listing 3 contains the modified AddBackupPathForOneProduct.vbs script, which includes the modified CheckError subprocedure.
Modifications to AddBackupPathForOneProduct.vbs. As callout A in Listing 3 shows, the first modification I made was to add the Option Explicit and On Error Resume Next statements to ensure that all variables are declared and all errors are trapped. Next, I declared the msiObject variable and set it to Nothing, as the code at callout B in Listing 3 shows. I set the variable to Nothing rather than an empty string because the variable will eventually hold an object. In the second line at callout B, I appended the CheckError command to invoke that subprocedure. I also appended this command to the line that callout C in Listing 3 highlights.
Modifications to CheckError. Because the modified subprocedure uses another variable, errRecord, the first modification I made was to add that variable's name to the Dim statement, as callout D in Listing 3 shows. Then, I added the If...Then...Else statement that callout E in Listing 3 highlights. Let's take a close look at this code.
By the time WSH reaches this part of the script, WSH has found an error and stored the details about that error in the strMessage variable. The If...Then...Else statement at callout E in Listing 3 determines whether this error relates to Windows Installer and, if so, adds data to strMessage. The statement accomplishes this task by first checking to see whether the msiObject variable has an assigned value. Remember that I initially set this variable's value to Nothing, so if msiObject isn't equal to Nothing, msiObject has a value (i.e., WSH successfully connected to a Windows Installer object). If msiObject has a value, the If...Then...Else statement calls the Installer::LastErrorRecord method and sets the errRecord variable to the object that the method returns. LastErrorRecord returns a Windows Installer Record object that contains data about the most recent Windows Installer error. (You can find details about the Windows Installer objects and methods I discuss this month at http://msdn.microsoft.com/library/default.asp?url=/library/psdk/msi/auto_8uqt.htm.)
The If...Then...Else statement then checks whether the errRecord variable is empty. If the variable isn't empty (i.e., the error is related to Windows Installer), the If...Then...Else statement uses the Record::FormatText method to retrieve the error data from errRecord and arranges the data into a readable format. The statement appends the readable data to the strMessage string.
At this point, the modifications stop and the standard CheckError code resumes. CheckError prints the string in strMessage and uses WScript::Quit to quit and return an error code of 1.
The script in Listing 3 shows how you can modify CheckError to trap and handle Windows Installer errors. You can modify CheckError to trap and handle data about other types of errors as well. You just need to locate the hexadecimal numbers for the errors you want to trap and incorporate the code in Listing 4 or Listing 5 into CheckError. Listing 4 shows the code for trapping one error. The hex value &H8000500D tells WSH to trap a certain ADSI error, but you can change this value to any applicable hex error number. Listing 5 shows the code for trapping more than one error. Once again, although the hex values &H8000500D and &H80000000 tell WSH to trap specific ADSI errors, you can change these values.
To incorporate this code into CheckError, replace the line at callout A in Listing 1 with the code in Listing 4 or Listing 5. Then, replace the commented lines that read Customized code goes here with code such as
" " & Hex(Err) & ": " & _
Err.Description & vbCrLf _
& "Custom error message"
where "Custom error message" contains the error message you want to display for that particular type of error.
A Practical Matter
I hope I've given you some practical ideas about how to trap errors in your WSH scripts. Explicitly declaring variables and using Option Explicit in your scripts is always a good practice to follow. Whether you go a step further by using a basic or advanced error-handling subprocedure depends on your needs.