Error Handling and Debugging
Overview
No explanation or tutorial of a programming language is complete without a thorough coverage of error handling . It is of course important to learn the syntax of a language and to use correct logic in your programs. What truly gives a program or script professional polish-what separates throwaway from production quality-is error handling.
Writing a computer program, even a simple one, is a delicate matter. You have to get the syntax exactly write. You have to place the quote marks and the parentheses just so. You have to name the files in a particular way. You have to follow a certain kind of logic. What's more, your program does not exist in a vacuum . A VBScript program can interact directly or indirectly with the scripting host, the operating system, the user, the network, and the Internet. A script is beset by the possibility of full disks, invalid user entry, scrambled file formats, and the electricity suddenly dropping out.
Things can, and will, go wrong.
Error handling is the programmer's line of defense against this inherent unpredictability . The term error handling refers not only to how a program responds when an error occurs, but also to how it prevents errors from happening in the first place.
The subject of debugging goes hand-in-hand with that of error handling. Debugging , as the name suggests, is the process of detecting, locating, and removing bugs from a program. The removing part of that process is most often the easiest part. The real art of debugging is in the use of tools and techniques for the detecting and locating processes. Basic proficiency with debugging brings with it a reduction in frustration and an increase in productivity.
This chapter will dive deep into the closely related subjects of error handling and debugging. If you are a new programmer, this chapter explains not only the VBScript mechanics of error handling and debugging, but also the universal principles and techniques at work. If you are an experienced programmer in other programming languages, this chapter is still worth your while; error handling and debugging in VBScript are quite unique and likely different from your experience with other languages.
Note that error handling associated with the Script Control (Chapter 18) is slightly different than with other hosts. Although error handling within the Script Control works similarly to other scripting hosts , there is also the possibility of handling errors via the host (for example the VB application itself), with a distinction between compilation and runtime errors. For more specific information, consult Chapter 18 on Script Control.
Types of Errors
There are three types of errors that can burrow their way into your lovingly crafted VBScript code. Syntax errors will halt the execution of the script. Runtime errors will invoke an error handler, giving you the opportunity to do something about the error-or at least display it attractively. Finally, logical errors will most commonly contaminate data in your application, cause strange behavior for users and administrators, and often cause other errors to occur.
Syntax Errors
VBScript, like all other programming or scripting languages, follows set rules for construction of statements. Before the script is run, the scripting engine parses all of the code, converting it into tokens. When an unrecognizable structure or an expression is encountered (for example, if you mistype a keyword or forget to close a loop), a syntax error is generated. Luckily, syntax errors can usually be caught during development phase, with minimal testing of the code.
Here is an example script that, when run under the Windows Script Host, produces the syntax error displayed in Figure 6-1.
Figure 6-1
Option Explicit Dim x If x > 0 MsgBox "That number is high enough." End If
Most of the time, the information in the error display will make it obvious where the problem is-in this case, a missing Then keyword on line 4. How the syntax error is displayed depends on which host the script is run under. For example, a syntax error for an ASP (Active Server Page) script run under Internet Information Services (IIS) would be displayed on an error page in the browser.
Syntax errors (and runtime errors) are easier to spot than logic errors (which we will look at shortly) because they always result in an error message being generated. Needless to say, with proper understanding of VBScript, syntax errors are not a major concern. Syntax errors tend to pop-up in several circumstances:
- When something is missing from the code-parentheses, keywords ( especially in blocks), statement elements, or when the keywords are simply out of place
- When a keyword is misspelled or used incorrectly
- When you try to use a Visual Basic or VBA keyword or function that is not implemented by VBScript
- When you use keywords that are not supported by a particular version of the scripting engine (certain keywords may be phased out, and others added)
As you may expect, dynamic code executed using the Eval , Execute , and ExecuteGlobal functions is not parsed at the same time as normal script code. Dynamic code is not parsed until the call to one of these functions, and so syntax errors in dynamic code are not reported until that time. Special attention has to be paid when generating dynamic code. Ideally , you would be able to test all of your dynamically generated code before releasing to users, but since dynamic code often includes input from outside sources, it is not always possible to anticipate syntax errors.
Appendix E shows all 53 of VBScript's syntax errors and their codes. All of these errors, with an exception of the first two-Out of Memory and Syntax Error-are relatively easy to diagnose and correct, though sometimes diagnosis can be tricky when you have a lot of nested parentheses or quote marks.
Runtime Errors
The second, and most common type of error is the runtime error. A runtime error occurs when a command attempts to perform an action that is invalid. A runtime error is different from a syntax error in that the offending code looks syntactically fine to the script engine, but has some kind of problem when it is executed. That is, the error does not necessarily appear while you are programming, but rather when you or one of your users is running the script.
Runtime errors can be divided into three categories: native VBScript runtime errors, non-VBScript runtime errors, and variable declaration runtime errors related to the Option Explicit directive. In all three cases, the result is the same: an error occurs while the script is running. What differentiates the three types is what causes an error-and how an error could be prevented.
Native VBScript Runtime Errors
For example, a runtime error occurs if you try to divide by zero ( ERR_DIVIDE_BY_ZERO.VBS ).
Option Explicit Dim x x = 200/0 MsgBox "The answer is: " & x
This code, run under the Windows Script Host, produces the error displayed in Figure 6-2.
Figure 6-2
Figure 6-3 shows the same error from the same script, but this time the script was launched from the command line (see Chapter 12).
Figure 6-3
As you can see, as with the syntax error, the default error display gives fairly specific information about what went wrong. What distinguishes this example from a syntax error is that there is nothing wrong with the syntax of this code. Instead, the code is trying to do something that computers don't like: dividing by zero.
Another common example of a runtime error is passing the Null value to a function that does not like nulls, as in this example.
Option Explicit Dim x x = Null MsgBox x
This code will produce an Invalid use of Null runtime error on line 4. The MsgBox function does not accept Null as a valid input value. Null values often cause problems when passed to built-in VBScript functions; so when your script takes in data from user input, a file, or a database, it is important to make sure that you test for Null values. One common technique is to take advantage of VBScript's implicit type coercion (see Chapter 3) to get rid of the Null .
Option Explicit Dim x x = GetValueFromDatabase() MsgBox "" & x
GetValueFromDatabase is a fictional function that might return a Null . This code accounts for this fact by appending an empty string to the value of x . When VBScript executes this code, the concatenation of the empty string causes the Null subtype of x to be converted to the String subtype. A little defensive programming can prevent many runtime errors from ever occurring.
The tricky thing with runtime errors such as these is that it takes some time for a programmer in any language or platform to learn the particular things that can cause runtime errors. Unfortunately, it would take an entire book, at least the size of this, one to cover all of the possible scenarios for runtime errors, especially when you consider all of the hosts under which VBScript can be run and all of the commercial and custom components that VBScript can use.
Here are two tips to help you with runtime errors: one, the first time you use a function, read the documentation for that function to look for possible traps that could lead to runtime errors, and then code defensively to prevent those errors. And two, if you are plagued by a runtime error you don't have an explanation for, search online knowledge bases and discussion forums; chances are someone has already encountered that error and can tell you what's causing it.
Non-VBScript Runtime Errors
The two examples of runtime errors we have looked at so far-a divide by zero error and a Null value passed to the MsgBox function-are produced by the VBScript runtime engine (which you can tell by the Source value of Microsoft VBScript runtime error in Figure 6-2). However, not all runtime errors will come from VBScript itself. In fact, depending on the complexity of the project, it is safe to say that most errors you encounter as a VBScript programmer will not be produced by the VBScript engine.
Instead, runtime errors will often come from other sources-namely, scripting hosts such as IIS and external components such as Microsoft's ActiveX Data Objects (ADO) component. Runtime errors can even be raised by other VBScript scripts since any script can use the Err.Raise method to generate runtime errors. ( Err.Raise is covered later in this chapter.)
The method of dealing with non-VBScript runtime errors is the same as for native VBScript runtime errors. Ideally, the error message itself will give you enough information to make the cause of the error obvious. For example,
Microsoft's ADO database access component (see Appendix L) has a runtime error with the description The specified table does not exist . The cause of this error is pretty clear from the description; evidently, the offending code referred to a database table that does not exist, perhaps the result of a misspelling in the code.
However, ADO also has several other runtime errors with descriptions such as Unspecified error , Operation aborted , and even Errors occurred . What are you supposed to do when faced with such ambiguous information? Unfortunately, the only way to work past these errors is through a combination of detective work and trial-and-error. When a runtime error does not offer useful hints as to what's going on, your best bet is to turn to online knowledge bases such as the one at microsoft.com and online discussion forums such as those offered at p2p.wrox.com .
Start by searching these resources for information about the error. If you can't find instances of people having encountered and fixed the error before, then post a description of your problem so that other developers can offer help. When you post, be sure to include code samples and as much information as possible about the context of what you are trying to do. The annoyance of runtime errors is an unfortunate downside of being a programmer, but not an insurmountable one, especially with the help of your fellow developers around the world.
Errors Related to Option Explicit
As discussed in Chapter 4, the use of the Option Explicit statement is critical to your success as a VBScript programmer. It is your best defense against strange, difficult to detect errors that can result from misspellings of variable names and mistakes related to a variable's scope. When using Option Explicit , if your code refers to a variable that has not be declared (or that has not been declared with the correct scope) then VBScript will produce an error, thereby allowing you the opportunity to fix a problem that otherwise may not have been detected at all.
However, the error you receive in these situations will be a runtime error-not a syntax error as you might expect. VBScript will report only the Variable is undefined runtime error when the offending code is executed- not when the code is compiled by the script engine. What this means is that it is important to test your code fully before releasing it into a production environment. Let's quickly look at an example.
The following Windows Script Host script ( OPT_EXPL_ERROR.VBS ), which uses the Option Explicit statement, has a missing variable declaration for the lngDay variable in the SpecialNovemberProcessing procedure.
Option Explicit Dim datToday Dim lngMonth datToday = Date() lngMonth = DatePart("m", datToday) If lngMonth = 11 Then SpecialNovemberProcessing(datToday) End If Private Sub SpecialNovemberProcessing(datAny) lngDay = DatePart("d", datAny) MsgBox "Today is day " & lngDay & " of November." End Sub
As you can see from the code, the SpecialNovemberProcessing procedure will only be called when the month of the current system date is November. If you run this code when your system date is anything other than November, then VBScript will not detect the variable declaration problem with lngDay in SpecialNovemberProcessing . If you wrote this code and tested it with a non-November month, then SpecialNovemberProcessing would never be called. However, after you have released the code to production and November finally rolls around, this code will produce a Variable is undefined runtime error, and you will have an embarrassing production error on your hands. If you are reading these words in a month that is not November, and you would like to see this behavior in action, first run this script and you will see that no error is produced. Then, change the 11 in this line to match whatever month your system clock says it is.
If lngMonth = 11 Then
Run the script after making the change, and you'll see that VBScript generates a Variable is undefined runtime error.
The way to prevent this from happening is to fully test your code to make sure that all paths of execution are exercised. Check your code for procedures and functions that are called only under certain conditions, and then force those conditions to exist so that you can make sure all of your code executes properly.
Logic Errors
You can think of logic errors as hidden errors. Logic errors, in general, will not produce any kind of error message. Instead, a logic error will produce what programmers usually call 'undesirable results.' For example, if you write a sales tax calculation script for processing orders on your ASP-based Web site, and that script incorrectly calculates the tax amount, that's a logic error-otherwise known as a bug. No error message is produced, but the program is wrong all the same. One could definitely make an argument that logic errors are the worst kind of error since they can go undetected for a long time, and, as in the example of miscalculated sales tax, can even cause serious legal and financial problems.
The computer generally cannot detect a logic error for you. Only careful examination, testing, and validation of program code can detect logic errors. The best way to deal with logic errors is to avoid them in the first place. The oldest and most common method for logic error prevention is requirements and design specifications, which are detailed descriptions (in the form of words and/or diagrams) of how a program needs to work. A requirements specification stays as close to normal human vocabulary as possible, without describing actual technical details, so that nontechnical subject matter experts can verify the requirements. A design specification is generally produced after the requirements specification and describes the details of the technical implementation that will 'solve' the requirements.
By producing specifications, you can theoretically avoid logic errors by ensuring that you fully understand the problem and the proposed solution before you start writing code. However, even with perfect specifications, logic errors can still creep into code. The programmer might accidentally use a 'plus' operator instead of 'minus,' or a 'greater than' operator instead of 'less than.' The programmer might forget to implement the special sales tax processing rules for a particular locale, even if those rules are clearly spelled out in the specifications. Logic errors can also result from improper use of the programming language. For example, a VBScript programmer who does not understand the subtleties of the Variant subtypes and implicit type coercion described in Chapter 3 could introduce logic errors into a script.
Because some amount of logic errors is inevitable in all but the most trivial programs, thorough testing of program code is essential. The programmer himself or herself has the first responsibility to perform some basic testing of his or her code, and ideally the programmer will have professional testers who can follow up with more methodical and thorough testing. Ideally, such testing is based on the requirements and design specifications on which the code is based.
In addition to upfront specifications and after-the-fact testing, another preventative measure against logic errors is what is known as defensive programming. Defensive programming is about checking assumptions about the expected program flow and either (a) generating runtime errors when those checks fail, or (b) including extra code that fixes the problem. For example, if you are writing a function that takes a numeric argument, and your logic requires that the argument must be greater than zero, include a double-check at the top of the function that ensures that the argument is greater than zero. If the check fails, you can choose to generate a custom runtime error or, depending on the situation, do something to fix the offending numeric argument. (We discuss custom runtime errors later in this chapter.)
Finally, the greater the complexity, the more likely that logic errors will be introduced. If you are working on a particularly complex problem, break the problem down into manageable chunks , in the form of procedures, functions, classes, and so on, as discussed in Chapter 4. When the solution to a problem is decomposed into smaller modules, it becomes easier to hold in your head all at once while simultaneously allowing you to focus your attention on one small aspect of the problem at a time.
Error Visibility and Context
A key aspect of understanding and managing runtime errors is knowledge of where your code is running and what happens when an error occurs. In the following sections we will briefly describe some of the more typical situations. Later in the chapter we will introduce 'error handling' techniques, which can be used to catch errors when they occur and control what happens after that point.
Windows Script Host Errors
Throughout the book so far, the example scripts we've been using are intended to be run under the Windows Script Host (WSH), which is a scripting host built into Windows. As we have seen in Figures 6-1, 6-2, and 6-3, WSH has an automatic error display mechanism. For 'interactive' scripts where the person running the script is sitting in front of the computer launching the script, this default error display may be sufficient. If an error occurs, WSH displays it in a dialog box (or as plain text on the command line, depending on how the script was launched), and the human operator can decide what to do at that point.
However, often a WSH script runs automatically, perhaps on a schedule, with no human operator sitting in front of the computer. In this case, you can control to some degree what happens when an error occurs instead of using WSH's default error display mechanism. The section Handling Errors later in this chapter discusses various techniques.
Server Side ASP Errors
A server-side ASP error can occur when IIS is processing ASP code. Even though ASP code is used for producing Web sites, the code actually runs on the server to produce Web pages and other Web-friendly files that are pushed out to the browser. It is important to distinguish this from client-side VBScript that actually runs inside of the Internet Explorer browser (see next section).
However, even though the code is executed on the server, paradoxically the default mechanism of displaying an error is inside the Web browser. If you think about it, this makes sense. Without the browser, IIS is basically invisible. Its whole purpose is to push content to the browser. There is no terminal in which to display an error on the server, and even if there was, most of the time no one is sitting there watching the server.
However, IIS does not leave you powerless when it comes to error display. The section later in this chapter called Presenting and Logging Errors describes a technique you can use to display how and whether detailed error information is displayed in the browser when a server-side error occurs.
Client Side VBScript Errors in Internet Explorer
Since client-side VBScript code runs inside of the Internet Explorer browser, naturally you would expect errors to be displayed within the browser. That is the case, but your users very likely will have their browsers set up so that they never see a client-side VBScript (or JavaScript or JScript for that matter) error when it occurs. Figure 6-4 shows the Advanced tab on the Internet Options dialog box for Internet Explorer, with the "Display a notification about every script error" option highlighted.
Figure 6-4
As you can see, users may or may not have error display turned on in the browser. It's safe to assume that your users do not have this option turned on because having it turned on becomes annoying after awhile. If an error message popped up in your browser every time you browsed to a Web page with a client-side script error, you would be driven crazy clicking the OK button all the time. When error display is turned off, a small yellow triangle with an exclamation point (!) will appear in the status bar at the bottom of the browser window.
This will be the user's only indication that an error has occurred, and the actual error message will only come up if the user happens to notice the yellow icon and clicks on it. However, it is important to consider the likely possibility that users of your Web page will not care what the error is. There is nothing that they can do about it anyway. All they know is that the page is not working. This situation underlines the importance of thoroughly testing all of your browser-based VBScript code.
Handling Errors
What exactly does 'error handling' mean? In the purest definition, error handling means take an active, rather than passive, approach to responding to errors. This means having extra code built into your script to deal with errors in case they occur. This can take the form of a 'global' error handling scheme that does something such as:
- displaying the error to a user
- logging the error to a file, database, or the Windows Event Log
- e-mailing the error to a system administrator
- paging the system administrator
- some combination of all of the these
In addition to a general error handling scheme, you can trap for specific errors at specific points. For example, trying to connect to a database is a common point where errors occur. The password entered by the user might be wrong, or the database might have reached the maximum allowable connections. Knowing that connecting to a database is error prone, the experienced VBScript programmer will put a specific error trap in his or her code in the place where the code attempts a database connection.
The remainder of this section will introduce the elements necessary for handling errors in your VBScript programs.
The Err Object
The Err object is what is described in the Microsoft VBScript documentation as an 'intrinsic object with global scope,' which means that it is always available to any VBScript code. There is no need to declare a variable to hold an Err object and no need to instantiate it using CreateObject or New . There is exactly one Err object in memory at all times while a VBScript program is running.
The Err object contains information about the last error that occurred. If no error has occurred, the Err object will still be available, but it will not contain any error information. Error information is stored in the properties of the Err object; some of which are given in the table.
The properties and methods of the Err object are described in more detail in Appendix E.
Description |
Holds a textual description of the last error that occurred |
Number |
Holds the number of the last error that occurred |
Source |
Holds a textual description of the source of the last error that occurred; usually this is the name of the component from where the error originated |
HelpFile |
If the source of the error has an associated Windows help file, holds the pathname of the help file |
HelpContext |
If the source of the error has an associated Windows help file, holds a unique identifier |
The Err object also has two methods. The first is the Clear method, which erases all of the properties of the Err object so that the information about the last error is thrown away. The second is the Raise method, which you can use in your VBScript code to generate custom runtime errors. The next section about the On Error statements goes into more detail on how you can use the Err object and its properties and methods to handle errors in your VBScript programs. In addition, the section later in this chapter called Generating Custom Errors explains the use of the Err.Raise method.
Using the On Error Statements
Unfortunately , VBScript does not have the robust, structured error handling mechanism offered by other programming languages such as Visual Basic, C++, Java, and the .NET languages. This is one of VBScript's most glaring shortcomings. It is not a stretch to characterize VBScript's error handling mechanism as awkward and limiting. In fact, if you don't understand how it works and use it properly, it can cause you to not even be aware that dozens of errors might be occurring in your code.
By default, a VBScript program does not have any error handling at all. All of the example scripts we have had in the book so far are in this default state. As described at the beginning of this chapter, if an error occurs, whatever error display mechanism the script host uses takes over and, in most cases, simply displays the error information to the user.
The most useful way to think about VBScript error control is that there is a switch that controls the error control setting for the entire script. The default position of the switch is On . When the switch is On , any error that occurs will immediately be reported . When the switch is
Off , any error that occurs will essentially be ignored (that is, unless you specifically check to see if an error occurred). This WSH script ( ERR_DIVIDE_BY_ZERO.VBS ) from the beginning of the chapter that causes a divide by zero error is using the default On position.
Option Explicit Dim x x = 200/0 MsgBox "The answer is: " & x
When the script engine tries to execute line 4, it hits the divide by zero error and immediately displays the error, as shown in Figures 6-2 and 6-3.
If you want to flip the error control switch to the Off position, you can add the On Error Resume Next statement to the top of the script, like so ( ERR_DBZ_RESUME_NEXT.VBS ):
Option Explicit On Error Resume Next Dim x x = 200/0 MsgBox "The answer is: " & x
If you run this code, instead of displaying the divide by zero error, VBScript ignores that error and continues executing the next line. The message box pops up, but since the value of x was never initialized , the value shown after "The answer is: " is blank. The divide by zero error still occurred, but with the switch in the Off position, VBScript won't tell us about it.
You can see now how flipping the global error control switch to the Off position with On Error Resume Next could get you into trouble. What if the x = 200/0 line was part of a much larger algorithm calculating the price of an expensive product sold by your company? If you had used On Error Resume Next in this manner to suppress error, then you might never find the error and the price of the product could be way off.
So we are not trying to say that On Error Resume Next is inherently bad. What we are trying to say is that because On Error Resume Next globally suppresses errors for your entire script-including all procedures, functions, classes, and (with ASP) include files-it is very dangerous and must be used carefully . We can propose with confidence the following two rules.
Note |
Unless you have a very good reason, never suppress all errors by simply placing the bfseries On Error Resume Next statement at the top of your script. |
And
Note |
If, in the body of your script, you use the On Error Resume Next statement to temporarily disable error reporting, make sure you use a corresponding On Error GoTo 0 statement to enable it again. |
What is On Error GoTo 0 ? Just as On Error Resume Next flips the error reporting to the switch to the Off position, On Error GoTo 0 turns it back on. Our second rule states explicitly what is implicit in the first rule: in general, don't use On Error Resume Next by itself, without a subsequent On Error GoTo 0 statement to flip the switch back to On .
Used together with the Err object (introduced in the previous section), the two On Error statements can be used to add specific error traps to your code. The following script ( ERR_TRAP.VBS ) demonstrates an error trap using On Error Resume Next and On Error GoTo 0 together. The example also demonstrates the Err.Raise method, but we'll be saving detailed discussion of Err.Raise until the Generating Custom Errors section later in this chapter. The script also contains an incomplete procedure called DisplayError() , which we will populate with real code in the next section.
Option Explicit Dim x On Error Resume Next x = GetValueFromDatabase() If Err.Number = 0 Then MsgBox "The value of x is: " & x Else DisplayError End If On Error GoTo 0 Private Function GetValueFromDatabase() 'Deliberately create an error for 'demonstration purposes. Err.Raise vbObjectError + 1000, _ "GetValueFromDatabase()", _ "A database error occurred." End Function Private Sub DisplayError() 'Stub procedure. We will fill this in 'with a proper error display. MsgBox "An error occurred." End Sub
The part of this code that we want to focus on is the block that begins with the On Error Resume Next statement and ends with the On Error GoTo 0 statement. By surrounding this block of code with these two statements, the programmer who wrote this code is saying 'There is a good chance an error might occur right here, so I'm going to set a trap for it.'
The line of code that the programmer is worried about is this one.
x = GetValueFromDatabase()
We created this fake GetValueFromDatabase() function to illustrate the point that database- related calls are often prone to errors. When databases are involved, there are just a lot of things that could go wrong-more so than in most other situations. The same could be said of interactions with the Windows file system, the Internet, e-mail, networks, or external components with which you are not familiar. Over time, programmers develop a sixth sense about hot spots for errors.
In this case, the programmer was correct: our fake GetValueFromDatabase() function does raise an error (the details of the Err.Raise method call will be covered in the Generating Custom Errors section later in this chapter). Whenever an error occurs, the Err object's properties are populated with information about the error. Generally , you can count on the fact that if an error occurs, the Err.Number property will be some number greater than or less than zero. So the line immediately after the call to GetValueFromDatabase() checks the Err.Number property.
If the error number is zero, then the code assumes that no error occurred and proceeds with its normal path of execution-in this case, displaying the value of the variable x . If an error does occur, the script attempts to gracefully display the error before continuing. (We'll put some meaningful code in this DisplayError procedure in the next section, Presenting and Logging Errors .)
What your script does after an error occurs really depends on the situation. You might want to log the error to a file. You may want to hide the error from the user. You may want to retry the operation a few times before giving up. You may want to send an e-mail to the system administrator. You may want to continue execution of the script after the error or stop the script. The choice is up to the programmer.
The key point here is this: if the programmer had not set this trap for this error and taken the action he or she chose (in this case, displaying the error), he or she would not have had any control over the situation. The VBScript host would have followed its default error handling path, as illustrated with the script errors at the beginning of this chapter. In summary, here are the steps for setting an error trap.
- Use the On Error Resume Next statement on the line immediately before the line you suspect might raise an error. This assumes that you have been careful not to use On Error Resume Next to turn off error handling for the whole script.
- On the line immediately after the suspect line, check the value of Err.Number . If it is zero, you can safely assume that no error has occurred and proceed as normal. If it is anything other than zero, an error has occurred and you have to 'handle' the error somehow. There are many choices for what to do, including displaying the error information to the user, logging it, e-mailing it, using a Do loop with a retry counter variable to try again a few times, or hide the error completely by ignoring it.
- After you have handled the error, it is very important to use the On Error GoTo 0 statement to put VBScript back into its default mode of handling errors for you. If you do not follow the On Error Resume Next statement with a corresponding On Error GoTo 0 statement, then you will, without realizing it, suppress errors later in your script.
It is unfortunate that VBScript's error handling is designed such that the programmer is forced to watch out for error 'hot spots' and set traps to catch the errors so that they can be 'handled' elegantly-rather than having VBScript or its host take over and stop your script dead in its tracks. The more robust error handling schemes of other programming languages have ways of setting 'generic traps' for errors so that you are always in control. VBScript does not have such a facility.
You may have already seen the big problem here: if VBScript does not give us a way of generically trapping errors, are we supposed to put specific error traps after every single line of code we write? Obviously, doing this for every line of code is not practical, but unfortunately you do have to use these error traps in places where errors are likely. You just have to trust, given proper use of VBScript, that your calls to generic functions like MsgBox() and InStr() are not going to raise errors, but when dealing with those 'hot spots' mentioned earlier, error traps are a good idea for production code.
It's not quite as bad as it sounds. Within any given script, you will have some hot spots, but hopefully not so many that you go crazy writing traps. In closing, here is an illustration of a section of script code with several possible hot spots in a row.
On Error Resume Next DoTheFirstThing() If Err.Number <> 0 Then HandleError() Exit Sub End If DoTheSecondThing() If Err.Number <> 0 Then HandleError() Exit Sub End If DoTheThirdThing() If Err.Number <> 0 Then HandleError() Exit Sub End If On Error GoTo 0
What we have done here is created a larger section of code with multiple traps, but we've used only one pair of On Error Resume Next and On Error GoTo 0 statements to block out the whole thing. As you can see, the If block that checks Err.Number is the same each time, so while we have more code than we'd like, Copy and Paste makes adding the extra code relatively painless.
Presenting and Logging Errors
As discussed in the previous section, when you have 'trapped' an error, you have to do something with it. This 'doing something' is usually referred to as 'handling' the error, which means you are going to respond to the error in some specific way other than letting the VBScript host handle the error on your behalf .
The most common error handling technique is to display the error to your users. As demonstrated at the beginning of this chapter, if you do not use the On Error Resume Next statement, VBScript's default error handling, depending on the host, is generally to display the error to the user somehow. So if VBScript will display the error for you, why add your own error handling and display code? There are two good reasons: control and cosmetics.
If you do not use the error trapping technique described in the previous section, then you are giving all error handling control to the VBScript host. Yes, the host will display the error for you, but it will also stop your script at the exact point the error occurred. If you had a file open , you won't get a chance to close it. If you were in the middle of a database update, your connection to the database, and your data, will be thrown away. If you were in the middle of collecting a large amount of information from the user, all of that work on the user's part will be lost. Allowing the VBScript host to handle all errors for you is often not the best technique.
The second reason mentioned, cosmetics, has to do with how the error is displayed. The error display in, for instance, the WSH, is not the most friendly in the world. Your users might miss the important information buried in the daunting error display dialog box. By writing your own procedure to display errors in your WSH scripts, you can present a more professional face to your users. Let's add some code to the DisplayError procedure we saw in one of the example scripts ( ERR_TRAP.VBS ) from the previous section.
Private Sub DisplayError(lngNumber, strSource, strDescription) MsgBox "An error occurred. Please write down " & _ "the error information displayed below " & _ "and contact your system administrator:" & _ vbNewLine & vbNewLine & _ "Error Description: " & strDescription & vbNewLine & _ "Error Number: " & lngNumber & vbNewLine & _ "Error Source: " & strSource, _ vbExclamation End Sub
This looks like a lot of code, but really we're just stringing together a nice, friendly message with line breaks and white space, as shown in Figure 6-5. Other than the improved appearance, we're basically displaying the same information that VBScript would have by default.
Figure 6-5
Beyond displaying the error, you have other options as well. In fact, if your script runs unattended, you might not want to display the error at all since there is no one sitting there to click the OK button. One of the most popular techniques is to log the error to a file, a database, or the Windows Event Log. You can also e-mail the error to a system administrator, or even page the system administrator on his or her beeper . You could get really fancy and send a message to a Web site that reboots the server in your back office. How elaborate you get, and what ways you choose to respond, is really dependent upon the situation-in particular, how important the script is and what bad things might happen if an error occurs without anyone noticing.
There is unfortunately not enough space in this chapter to demonstrate all of the possible techniques for logging, e-mailing, and so on, but none of these ideas is beyond the capabilities of VBScript and the companion components described in this book and elsewhere. You can use the FileSystemObject library (see Chapter 7) to open an error log file and append to the end of it. You can use Microsoft Outlook and Exchange to send an e-mail or beeper message. You can use IIS to redirect to another Web page.
The key thing to keep in mind is to retain control of your application, its behavior, and its appearance by taking a proactive stance about what you want to do when errors occur.
Server Side ASP Errors
One common VBScript-related error handling situation bears special mention: the display of server-side ASP errors using IIS. As described earlier in this chapter, by default, IIS will by default push ASP syntax and runtime errors out to the browser. IIS uses a built-in template HTML page for displaying a server-side error in the browser. Figure 6-6 shows a divide by zero runtime error for an ASP page.
Figure 6-6
As you can see, the normal error information is displayed as you would expect (although it's kind of buried in the fine print of the page): the error number, description, source, and so on. In addition, the display includes information about the page that caused the error and about the requesting browser. It's nice that IIS has this built-in error display feature, but this default page is not the most attractive in the world, and almost certainly does not fit in with the look and feel of the rest of your Web site.
You do not, however, have to keep this default error display page. For each Web site hosted by your IIS server, you can set up custom error handler pages for each type of error that can occur. As you can see in Figure 6-6, the error type for a server-side ASP code error is HTTP 500.100 - Internal Server Error - ASP error . This page is used to display ASP and VBScript runtime errors, as well as errors raised by other code called by your ASP code. If you want to provide an error display page of your own, you can replace the default error page in IIS for 500.100 ASP errors (and many other kinds of errors as well, though errors other than 500.100 are not in the scope of this discussion).
Figure 6-7 shows the Web site properties screen for the IIS default Web site. Each Web site configured in IIS has its own unique properties, so you can set up different custom error display pages for each of your Web sites. For the Web site properties in Figure 6-7, the default 500.100 error page is configured. By clicking on the Edit Properties button, you can point to a different error display file. (See Chapter 17 or the IIS documentation for more information about configuring your Web site.)
Figure 6-7
Before you can replace the default page, however, you must create a custom error display page. The best way to go about this is to make sure you understand the basics of ASP. If you do not, Chapter 17 in this book can get you started. If you have at least a basic grasp of basic ASP concepts, then the best thing to do is to use the default 500-100.asp Web page, the location of which is highlighted in Figure 6-7, as a guide.
The key is to use the Server.GetLastError method to get a reference to an ASPError object, which is very similar to the native VBScript Err object. The ASPError object has properties like Number , Description , and Source , just like the Err object. In addition, the ASPError object it has properties called ASPCode and ASPDescription that return more detailed information if the error was an ASP-specific error raised by IIS. There are even File , Line , and Column properties to provide information on exactly where the error occurred. The following code snippet illustrates how to get a reference to an ASPError object.
<% Option Explicit Dim objASPError Set objASPError = Server.GetLastError %>
It's a safe assumption (though not a guarantee) that Server.GetLastError will return a fully populated ASPError object- otherwise this ASP page would not have been called. Now that you have a reference to an ASPError object, you can embed code like the following within the HTML of your new custom error page to display the various properties of the ASPError object.
Response.Write "Number: " & _ Server.HTMLEncode(objASPError.Number) & " " Response.Write "Description: " & _ Server.HTMLEncode(objASPError.Description) & " " Response.Write "Source: " & _ Server.HTMLEncode(objASPError.Source) & " "
This is a very simple example, and, as you will see if you read the default 500-100.asp file, you can get quite elaborate. A custom error page is a great way to centralize your error handling code. Keep in mind that since this is your ASP error display page on your Web site, you can use the error information in the ASPError object any way you like. You could provide an input field and a button to allow the user to type in comments and e-mail them and the error information to you. You might even decide to hide the ugly details of the error from your users, replacing it with a simple Sorry, the Web site had a problem message, and logging the error in the background. You can also include code for handling specific errors, taking different actions depending on the error number. As in any error handling situation, it's all up to you.
If your Web site already has an established look and feel, it's a good idea to design your error page to match the rest of your Web site. This will reduce the surprise of your users if ASP errors do occur on your Web site since the error display won't make them feel like they've been suddenly kicked out of your Web site.
Generating Custom Errors
So far in this chapter we have been discussing how, when, and why to react to errors coming out of other people's code. However, VBScript also provides the ability to 'raise' errors yourself. Any VBScript code you write can at any time stop execution and generate an error. The key to this ability is the Raise method of the Err object we introduced earlier in this chapter.
Using Err Raise
You might remember that the Err object is always available and is of global scope. That means you can refer to it any time you want without having to declare a variable for it. The following code demonstrates a call to Err.Raise .
Err.Raise vbObjectError + 10000, _ "MyScript.MyFunction", _ "The Age argument of MyFunction may not be greater than 150."
In this code example we are using the first three arguments of the Err.Raise method.
The first argument is the error number. You are free to any error number between 0 and 65535, but using zero is not recommended since, as we illustrated earlier in this chapter, many programmers will consider 0 as the absence of an error. In addition, Microsoft strongly recommends that you add the value of the vbObjectError constant to your error number. The detailed explanation of why this is necessary is out of the scope of this book, but to summarize, adding vbObjectError to your error number makes sure your error number will not clash with 'official' Microsoft error numbers .
The second argument is the error source. This is an optional parameter that many people omit or leave blank, but if you are raising an error from a specific script, page, and/or function, including some kind of description of where the error originated, is a good idea. A programmer who receives your error (maybe even yourself) will thank you someday for filling in the Source argument.
The third argument is the error description. It's a good idea to be as specific as possible in your description. Include any additional information you can think of that would be helpful to someone trying to diagnose and eradicate the cause of your error. Remember those useless ADO error descriptions we mentioned earlier in the chapter? Don't stick yourself or your fellow programmers with useless messages like Errors occurred or Undefined error .
The Err.Raise method accepts two additional arguments that are seldom used in a VBScript context: helpfile and helpcontext . If your VBScript project does have a companion Windows help file, by all means include the path to it in the helpfile argument. And if your help file has a specific section that explains the cause and solution for your custom error, then providing the 'context id' for that section is a great idea too.
When Not to Use Err Raise
Now that you know how to generate a custom error, the obvious follow-up questions are why and when to raise a custom error. First, though, let's talk about when it's not a good idea to raise custom errors. Errors are generally created for the benefit of other programmers. When something goes wrong (like a lost database connection), or when a programmer tries to do something illegal (like dividing by zero), an error is the most common way to inform a program or programmer of that fact.
However, your users generally do not appreciate errors. Ideally, the only time a user should see an error is when something unexpected occurred either in your script or in a script or component being used by your script. Furthermore, you want errors to make it only as far as the user 's eyes when you did not have a specific way of dealing with the error. (For example, if your script received an error indicating a lost database connection, you could try to reconnect to the database rather than stopping your script and displaying the error to the user.)
It is useful to distinguish between an error and a 'problem message.' Certainly, there are times that you must communicate bad news to your user, or ask your user to fix some problem. You see this all the time on Web pages with forms. If you forget to fill in a certain field, the Web page will tell you about it so that you can fix the problem. This kind of 'problem message' is different from an error.
Remember the script at the beginning of this chapter that caused a divide by zero error? What if you had a script asking the user to enter a number which your script will divide into 100, like so:
Option Explicit Dim x, y x = InputBox("Please enter a number to divide into 100.") y = 100 / x MsgBox "100 divided by " & x & " is " & y & "."
What if the user enters 0? With this code as-is, run under the WSH, the user will see an unfriendly divide by zero error, as seen in Figure 6-2.
What if, instead, you tested the value that the user typed in before attempting the division, as in this script ( PROBLEM_MESSAGE.VBS )?
Option Explicit Dim x, y x = InputBox("Please enter a number to divide into 100.") If x <> 0 Then y = 100 / x MsgBox "100 divided by " & x & " is " & y & "." Else MsgBox "Please enter a number other than zero." End If
This time, the user sees a nice, friendly 'problem message' instead of an ugly, scary error. Users would always prefer to see a cordial message that informs them of what they can do to fix a problem rather than an error that leaves them feeling stupid and helpless.
The point of all this is to say that it is not a good idea to abuse Err.Raise by using it to inform your users of when they have done something wrong. Instead, as described in the next section, use Err.Raise to catch programming problems or to report problems in the environment. The following script ERR_MSG_UGLY.VBS ) illustrates how not to use Err.Raise . The error description is sarcastic , but it reflects how your users will feel if you use Err.Raise in this way.
Option Explicit Dim x, y x = InputBox("Please enter a number to divide into 100.") If x <> 0 Then y = 100 / x MsgBox "100 divided by " & x & " is " & y & "." Else Err.Raise vbObjectError + 15000, _ "ERR_MSG_UGLY.VBS", _ "Hey, stupid, you can't enter a zero! It will " & _ "cause a divide by zero error!" End If
When to Generate Custom Errors
Now that we've discussed how to generate custom errors-and when not to-the question that is left is when should you generate a custom error? The answer has to do with assumptions. Whenever we write computer programs, we are forced to operate on a set of assumptions. Different types of programs running in different type of environments have different sets of assumptions, but assumptions are always there.
Certain assumptions are foundational: you assume a certain operating system and computer configuration; you assume the ability to open files for reading and writing; you assume a certain database configuration; you assume that the Customer table is structured in a certain way; you assume that the Web server works in a certain way. Other assumptions are more specific, and often take the form of rules; for example, you assume that the CustomerID argument passed into your LoadCustomer function is never less than or equal to zero and that the number will match an actual record in the Customer table.
The concept rules is closely related to the concept of assumptions. For example, there is a rule that you cannot divide by zero. If you try to use VBScript (or just about any other programming language) to divide by zero,
VBScript is going to generate an error. To put it another way, VBScript 'assumes' that you are not going to try to divide by zero, and if you do, VBScript is going to generate an error. And therein lies the key point when it comes to assumptions, rules, and errors: rather than simply trusting that all VBScript programmers will never attempt to divide by zero (which would no doubt cause troubles at the operating system and even CPU levels), the VBScript compiler/runtime engine double checks any division operation to make sure that the code is not attempting to divide by zero. If the code is attempting it, VBScript generates an error: dividing by zero is not allowed.
A rule was broken; an assumption failed. Generate an error. That is the most basic answer to the questions of when and why to generate custom errors.
Before we look at a specific example, let's reconsider the previous section, which explained that it is not a good idea to use custom errors as a way to tell your users about things they have done wrong. Instead, we suggested using more friendly and helpful 'problem messages,' which are not the same as 'errors' per se. The distinction between this advice from the previous section about when not to use errors to report a problem and the advice in this section about when you do want to use errors to report a problem is one of 'audience.'
Take another look at this ERR_MSG_UGLY.VBS script.
Option Explicit Dim x, y x = InputBox("Please enter a number to divide into 100.") If x <> 0 Then y = 100 / x MsgBox "100 divided by " & x & " is " & y & "." Else Err.Raise vbObjectError + 15000, _ "ERR_MSG_UGLY.VBS", _ "Hey, stupid, you can't enter a zero! It will " & _ "cause a divide by zero error!" End If
Besides the obviously rude wording of the error message, the reason this technique is undesirable is that the 'audience' of the error in this script is the user. Ideally, we don't want to use errors to communicate with our users. If we must report bad news to the user or tell him or her that he or she did something against the assumptions/rules of our script, we want to do this in a more friendly and helpful manner. The reason we say that the 'audience' of this script is the user is that the Err.Raise method is being used in code that is directly interacting with a user-as opposed to lower level code, in a subprocedure or class method.
However, when the 'audience' of a block of code is other code (or to put it another way, the programmer himself or herself), then an error is appropriate-because we can assume that the programmer is equipped to do something about an error. Take a look at this reworking of the script ( ERR_MSG_NICE.VBS ).
Option Explicit Dim x, y x = InputBox("Please enter a number to divide into 100.") On Error Resume Next y = DivideNumbers(100, x) If Err.Number = (vbObjectError + 15000) Then On Error GoTo 0 MsgBox "Please enter a number other than zero." Else On Error GoTo 0 MsgBox "100 divided by " & x & " is " & y & "." End If Private Function DivideNumbers(dblNumber, dblDivideBy) If dblDivideBy = 0 Then Err.Raise vbObjectError + 15000, _ "ERR_MSG_NICE.DivideNumbers()", _ "Division by zero not allowed." Else DivideNumbers = dblNumber / dblDivideBy End If End Function
Notice that we have moved our division code into a function called DivideNumbers . Our script has two logical sections now. In terms of 'audience,' the top part of the script is at the 'outer edge' of the script, directly interacting with the user. The DivideNumbers function, on the other hand, is lower down in the script, interacting only with other code. Since the 'audience' of the lower level DivideNumbers function is other code, if something goes wrong, the best way for DivideNumbers to tell the other code about it is with a runtime error.
However, the 'outer edge' code that is interacting with the user anticipates that this error could come back from the DivideNumbers function and has a trap to catch it if it does. The whole purpose of the error trap is to turn the unfriendly runtime error into a friendly 'problem message' for the user. The 'outer edge' code and the lower level code are communicating the same problem in two different ways based on the 'audience.'
The other thing to take notice of with the DivideNumbers function is that it is proactively verifying assumptions. This is what programmers call defensive programming, which involves anticipating problems and failed assumptions as early as possible. It is as if the programmer of DivideNumbers said to himself or herself, 'The primary purpose of this function is to divide two numbers. The division operation has an assumption/rule that division by zero is not allowed. Therefore, I am going to test this assumption, and if the test fails, I will raise an error.' This is one of the primary situations in which it is good to generate custom errors from your code.
Before moving on, let's consider another situation in which generating a runtime error would be appropriate. In the example above, the DivideNumbers function is doing a before-the-fact verification of an assumption. In other situations you must instead report failed assumptions after the fact.Consider a function called GetCustomerName , which takes a CustomerID argument and returns a customer name as a string. The function uses the CustomerID value to query the Customer table in a database. If the customer record is found based on the CustomerID argument, the function returns the customer's name from the database record. What if, however, no Customer record was found with that CustomerID ?
One way to handle this is for GetCustomerName to use Err.Raise to generate a custom No Customer record with specified CustomerID runtime error. Then the code that called GetCustomerName can respond to this error in whatever way is appropriate given the situation. There are other examples you can think of along these lines. If a procedure called OpenFile can't find the specified file, it might generate a File Not Found error. If a procedure called WriteFile is denied access to the specified file based on security settings in the file system, it might generate a Permission denied error.
At this point, we have discussed how to generate custom errors using Err.Raise , when and why to generate them, and when not to. Other than the syntax rules for the Err.Raise method, none of the restrictions or suggestions we have offered are built into VBScript itself. Instead, we have primarily laid out philosophical principles and techniques followed by programmers in many different languages. Learning and understanding these principles and techniques is an essential part of writing programs with a professional polish.
Debugging
The term debugging refers to the activity of a programmer trying to figure out the cause of a problem (or 'bug') in his or her program, and then taking steps to eliminate the problem. There are two steps to this process: first, identifying the problem; and second, fixing the problem. Even though in the formal sense the term debugging refers to both steps ( finding followed by fixing ), our discussion in this section will focus on the first step, which involves a certain amount of detective work and puzzle solving.
What is a Debugger?
A debugger is a tool to help programmers follow the logic of their code as it runs. This can aid not only in finding bugs , but also in simply understanding a program. If you can follow along with the code as it runs, you can more easily see how it works- especially if the program's logic twists and turns in ways that are difficult to comprehend by simply reading the code.
A debugger sits in between the compiler and/or runtime engine and the actual running program. Without a debugger, you write the code, compile it, and then run the resulting binary program. (In the case of VBScript, the compiling step is invisible; the scripting engine compiles the code for you when you run it.) When running the program, you can't tell exactly what's going on in the background-the program just runs according to the logic laid out in the code. A debugger, however, gives you a window into what's actually going on in the background as the code is run line by line.
A typical debugger gives you various tools to help in watching and interacting with your code as it executes:
- A way to specify a certain line of code on which to pause program execution. The line of code you specify is called a breakpoint . For example, if the problem you are having is deep down in the code in your CalculatePrice function, you would mark a certain line in the CalculatePrice function as a breakpoint. The program would run up until the breakpoint is reached, at which point the program pauses execution, showing you the breakpoint line.
- A way to step through your code one line at a time. Once the debugger has paused the program at a certain line because of a watch or breakpoint, the debugger allows you to slowly and deliberately execute the code one line at a time. This allows you to follow along with the logic of the program as it executes. Debuggers usually have three 'step' options: Step Into , Step Over , and Step Out . Which one you use depends on what you want to do-or more specifically , exactly which code you want to see. Step Into will bring the debugger into a procedure or function, if one is being called and if the code for it is in the scope of the debugger (in other words, you can't Step Into the VBScript MsgBox function, since that code is not available). This is useful when you suspect the procedure being called might have a problem. Step Over will do the opposite : if the line you are on calls a procedure or function, the debugger will execute it without stepping into it. This is useful when you are confident that the procedure being called is not part of the problem you're debugging.Finally, Step Out will finish the execution of a lower level procedure or function and then pause again at the next line of code after the call to that procedure or function. This is useful if you've stepped into a procedure and then decided that you don't need to step through the rest of it.
- A way to view the value of all of the variables active at any given point in the code. While code execution is paused, mostly any debugger will have some way to view all of the variables active at that point in the code and what their values are. A good debugger will even allow you to change the value of a variable so that you can experiment and observe the effects as you step through the code.
- A way to view the 'call stack.' , The call stack is the nested sequence of function and procedure calls that have led to a certain point in the code. For example, if the debugger has paused because of a breakpoint or watch in the low-level CalculatePrice function, the debugger's call stack window would show you that the Main procedure called the LoadOrder procedure, which called the CalculateOrder procedure, which called the CalculateLineItem procedure, which called the CalculatePrice function in which the debugger is paused. This tells you how you got to where you are.
Please note that the Microsoft Script Debugger discussed below does not have two options that developers may be accustomed to having in a debugger. First, the Script Debugger does not have a 'Locals Window,' which is a graphical display of all of the variables active at a breakpoint. (You can, however, use the Command Window in the Script Debugger to view the value of any in-scope variables.) Second, the Script Debugger does not have 'Watch' functionality, which is a way to set up a dynamic breakpoint whenever a certain variable changes value in a certain way.
Options for Debugging VBScript Code
The following forthcoming sections explore the details of debugging your VBScript code under the following scenarios:
- Scripts hosted by the Windows Script Host (WSH)
- Client-side Web scripts running in Internet Explorer (IE)
- Server-side Active Server Pages (ASP) Web scripts running under Internet Information Services (IIS)
- When a debugger is not available or convenient
Microsoft offers a freely downloadable product called the Microsoft Script Debugger, which offers all of the standard debugging features described in the previous section. This debugger is integrated with the three major VBScript hosts covered in this book: Windows Script Host, Internet Explorer, and Internet Information Services/ASP.
Before we get into the details of the Microsoft Script Debugger, the next section, Debugging Without a Debugger , describes concepts and techniques you can use when you do not have a debugger available. Sometimes you may find yourself in a situation where the use of a debugger is not practical or possible. This situation is common with scripts that somehow run across a network. For example, you may need to debug an ASP page hosted on another server to which you do not have access.
However, even if you are planning to use the Microsoft Script Debugger under WSH, IE, or IIS, you will most likely find the Debugging Without a Debugger section worthwhile since it contains techniques that are quite useful even when you are using a debugger.
Debugging without a Debugger
Often times the VBScript programmer is without the benefits of a debugger. In these times, there are some basic techniques that can assist with debugging, whether the goal is finding a problem or just understanding how a program works. Not all techniques, however, are available or useful in all hosts. As we discuss the techniques, we will identify which techniques are available in the three most common VBScript hosts: WSH, IIS with ASP, and IE.
Some of these techniques are useful when a debugger is available. In addition, you do not necessarily need to wait to implement these techniques until you have a problem that needs debugging. Once you have determined which of these techniques can be useful to you, build them into your application from the beginning.
Using a Global Debug Flag
This is a technique available in any host, though the implementation will be different depending on the host. The purpose of a global debug flag is to allow you to run your script in two different 'modes': debug mode and production mode. In debug mode, the script may, for instance, output various messages about what is going on in the program (see later), whereas in production mode, those messages would be suppressed. Debug mode is for the benefit of only the programmer or tester, not for end users.
A global debug flag is simply a Boolean variable that has global scope in your script. For example, you might call this variable gblnDebug . Many people prefer to implement a debug flag as a named constant, in which case you might instead call it DEBUG_MODE . (As we discuss debug flags, we'll use the named constant convention since a named constant is more reliable in that its value cannot be changed while the script is running.) In some hosts, such as IIS with ASP, you don't use a variable at all, but rather a custom property of the Application object (see later).
Once you have defined a global debug flag, anywhere in your script you can insert code that executes only when the flag is equal to True . It's important to have only one place in your script that sets the value of the flag so that it is consistently either On or Off throughout the script. The code to implement the other techniques discussed later, such as outputting debug messages, would go inside of an If End If block that checks the value of the debug flag.
Here is a simple example of a debug flag implemented in a WSH script ( WSH_NODEBUG_FLAG.VBS ).
Option Explicit Private Const DEBUG_MODE = True If DEBUG_MODE Then MsgBox "Script starting." End If MsgBox "Non-debug script code executing here." If DEBUG_MODE Then MsgBox "Script ending." End If
Here we have a named constant called DEBUG_MODE that, since it is declared at the top of the script, has global scope. Then, we have two debug messages at the beginning of the script and the end of the script. (See the next section for more information about debug messages.) These messages will only be displayed when DEBUG_MODE is True . The programmer can then change the value of the DEBUG_MODE constant to True or False , depending on whether he or she wants to see the debug messages or not. Of course, you have to be careful to make sure you set the flag to False before releasing a script to production.
Implementing a debug flag in with ASP or IE is a little trickier. With ASP, the most common technique is to use the Application_OnStart procedure in the global.asa file to add a custom property to the Application object's Contents collection. (See Chapter 17 for more information on these ASP concepts.) Then, anywhere in your ASP application, you can check the value of the debug flag on the Application object. Here is an example of what the code would look like in global.asa to create and set the debug flag.
Sub Application_OnStart Application("DEBUG_MODE") = True End Sub
Since the Contents collection is the default property of the Application object, it is conventional to not refer to it directly. Instead, as you see here, just follow the Application with parentheses. The Contents collection will automatically add a property called DEBUG_MODE with the specified value (in this case True ). Again, just as with the WSH example, the programmer changes the value of the flag to switch back and forth between debug mode and production mode. Finally, since the Application_OnStart procedure executes only when the Web site is stopped and restarted, many programmers will also create a special-purpose ASP page to change the value of the flag while the application is running.
Using a debug flag in IE is tricky because your scripts are running in the browser on a client machine. How you implement a debug flag for visibility within IE depends on how you are producing your Web pages. If your pages are static or generated by some kind of publishing tool, you will need to declare a variable in each page or template, and then change the value through Search and Replace , or whatever makes sense given your toolset.
If your pages are dynamically generated through ASP, then it's a little easier. Make sure that each HTML page you produce includes a declaration for the debug flag variable with page-level scope, which will ensure that all of the scripts on your page can read the variable to, for instance, include debug messages in the page output. One way to accomplish this is to put the variable declaration and value setting in an include file that is used by all of your ASP pages. To make maintenance of the flag as easy as possible, tie the setting of its value to a "DEBUG_MODE" flag on the Application object, as described earlier.
Outputting Debug Messages
What a debugging tool offers is visibility into the information inside of a program, such as variable values and when they change and the path (s) of execution through the code. You do not have that visibility when a debugger is not available to you. So what you need is a way to gain some visibility. The best way to do this is with debug messages.
Debug messages can be used for any number of purposes, depending on what information you need. For example, you can output debug messages in the form of log entries, which together provide a view into the sequence of events in your script. A log entry type debug message might report significant program events such as the beginning and ending of a script, an entry into a certain function, a connection to a database, the opening of a file, or the changing of certain variables.
Another use of debug messages is to track the changing of important variables. If you suspect that a certain variable is, because of a logic error in your code, changing to the wrong value at the wrong time, you can output a message each time the value is changed. This can substitute for the 'watch' feature of a real debugger.
Finally, debug messages are useful for displaying the total state of your program. For example, in an ASP program you could output all of the values stored on the Request , Response , Session , and/or Application objects at the bottom of each page (see later). Or, if you are using an array or Dictionary to store a series of values, you could output all of the values in a debug message.
Debug messages can take different forms. Like debug flags, the implementation depends on the host. The simplest way to output a debug message is with the MsgBox function, which displays a dialog box and pauses the execution of the code until the OK button is clicked. However, the MsgBox function is really useful only for scripts running in WSH or IE.
For WSH scripts, it's actually preferable to use the Echo method of the Wscript object (see Chapter 12 for detailed information on the intrinsic WSH objects) instead of MsgBox . The advantage of this is that you can choose to run the script either with wscript.exe , in which case the Echo method will display the message with a dialog box, or with cscript.exe , in which case the message will be output to the console window (see Chapter 12). If you use Echo a lot in your script, running it under cscript keeps you from having to click the OK button over and over.
This code fragment shows a debug message implemented with the Echo method.
If DEBUG_MODE Then WScript.Echo "Current order status: " & gstrOrderStatus End If
Yet another way to output debug messages in WSH is to use the StdOut property of the WScript object, which is implemented as a TextStream object (see Chapter 7). However, you can use only StdOut with WSH scripts that are exclusively run in the console window with cscript.exe . Using StdOut under wscript.exe will cause an error. Here is a code fragment that uses StdOut for debug messages.
Dim objStdOut If DEBUG_MODE Then objStdOut = WScript.StdOut End If ... If DEBUG_MODE Then objStdOut.WriteLine "Current order status: " & _ gstrOrderStatus End If
Finally, in WSH scripts you can also use the LogEvent method of the WshShell object to add events to the Windows Event Log. LogEvent can be used under wscript.exe or cscript.exe . Chapter 17 describes the LogEvent method in detail.
Since ASP scripts run on the server, MsgBox is not a good way to implement debug messages when IIS is the host. In addition, the WSH WScript and WshShell objects are not available in ASP. Instead, with ASP the best thing to do is either to include debug messages in the body of the HTML page or to log them to a file, database, or the Windows Event Log.
One powerful technique is to create an include file for use in all of your ASP pages that will include a standard section of debug info at the bottom of every generated HTML page. When the Web site is running in debug mode, the HTML will include the debug section. When in production mode, the debug section is omitted so that your users never see it.
This debug info section could be as simple or as fancy as you require. Most commonly, programmers will at least include information such as the names and values of all of the properties in the Contents collections of the Session and Application objects and the values stored in the Form , Cookies , QueryString , and ServerVariables properties of the Request object (see Chapter 17). If you have any other variables that store particularly important information, then you can include the values of these variables as well.
Homemade Assertions
VBScript unfortunately does not support a feature called assertions , which is included in many other languages, including Visual Basic. An assertion is a test of an assumption in the form of a True/False expression. For example, if in a complex algorithm you wanted to make sure that a certain variable is equal to a certain value at a certain point, in order to verify that the algorithm is working properly, you could add an assertion that tests the value of the variable. The assertion expression is evaluated only when the program is running in debug mode. In a language with a debugger and full support for assertions, the failure of the assertion would trigger a breakpoint pause in the debugger.
Even though VBScript does not natively support assertions, you can still 'roll your own.' The following code fragment illustrates an assertion in a WSH script.
If DEBUG_MODE Then If gstrOrderStatus <> "PENDING" Then WScript.Echo "***INVALID ORDER STATUS***" End If End If
Inserting Temporary Test Code
Finally, you can also include test code that you want to execute only while in debug mode. This is especially useful when you are trying to track down a stubborn logic bug in your code. A good debugger would allow you to pause the code at a certain point and execute test code that is not normally part of the program. Once again, if you do not have a debugger, you can use a debug flag to execute only test code while running in debug mode.
If DEBUG_MODE Then 'Test code to illegally change order status 'to verify status tracking logic. 'A status of NEW would not normally be allowed 'at this point. gstrOrderStatus = "NEW" End If
You might also want to mark these blocks of test code with a unique value in a comment so that you can use your editor's Search function to find and eliminate them later.
Debugging WSH Scripts with the Microsoft Script Debugger
In this section we will introduce the basics of activating the Microsoft Script Debugger for scripts running under the WSH. We will only explain how to activate the debugger when running a WSH script. Since the usage of the Script Debugger, once activated, is basically the same no matter which host you're running under, usage details are covered separately in a later section called Using the Microsoft Script Debugger . This section also assumes that you have read the earlier section called What Is a Debugger? which explains the basic terms and concepts of a debugging tool.
If you have not done so yet, you can download the Script Debugger for free from msdn.microsoft .com/scripting (be sure to get the version that matches your version of Windows). Even though the installation program does not require it, it's a good idea to reboot your machine after installing the Script Debugger.
Unlike IE and ASP/IIS scripts (see next two sections), enabling the debugger is not a configuration option. The WSH is automatically aware of the debugger. However, the WSH wscript.exe and cscript.exe programs require special command line switches in order to enable the debugger when running a script. (See Chapter 12 for more details on wscript.exe and cscript.exe .)
Normally, for non-command-line scripts if you want to run a WSH script, you would probably just double-click the .VBS file in Windows Explorer. However, if you want to debug a non-command-line WSH script, you have to launch the script using wscript.exe directly. The most straightforward way to do this is with the Run option on the Windows Start menu. If you work with command line scripts, you are already accustomed to launching your scripts with cscript.exe . In that case, all that's required to enable the debugger is to add an additional switch to your normal cscript.exe command lines.
Both wscript.exe and cscript.exe accept two debugging related switches. The behavior is the same for both wscript.exe and cscript.exe . The //x switch will launch the script in the debugger, making an automatic breakpoint on the first line of the script. The //d switch will launch the script as normal, but in a 'debug aware' mode, meaning that the debugger will only be activated if one of two things happens: if an unhandled error occurs or if the Stop statement is encountered .
Let's look at a few examples. We're going to use wscript.exe for these examples, but everything said applies equally to cscript.exe .
Before we get into the examples, however, we have to mention one quirk with the WSH and the Script Debugger: in order for debugging to work, the Script Debugger must already be running when you launch the script. You will have to start the Script Debugger manually before you launch your script for debugging. The executable file for the latest version of the Script Debugger that runs under Windows 2000 and Windows XP is msscrdbg.exe . The default installation location is C:Program Files Microsoft Script Debugger , but may be installed in a different folder on your computer.
Once you've located msscrdbg.exe , double-click it to launch the debugger. You may wish to create a shortcut for it to make it easier to launch in the future. The examples in this section assume that you already have the debugger running.
Our first example will illustrate how to launch a script in the debugger with a script that does not have any preset breakpoints or runtime errors. The downloadable code for this chapter includes a script called WSH_DEBUG_X.VBS . Place this file in a folder on your hard drive and use the Start ( Run menu option with this command.
wscript //x c:scriptsWSH_DEBUG_X.VBS
You will of course have to change the path for the script to match where you've placed it on your hard drive. When you run this command, you should see a window like the one shown in Figure 6-8.
Figure 6-8
This is the Script Debugger in an active debugging session. The //x command line switch tells WSH to run the script in the debugger with a breakpoint on the very first line. When you use the //x switch, you are in essence saying that you wish to debug your whole script from the very beginning. With //x , it is not necessary to manually specify any breakpoints.
The section called Using the Microsoft Script Debugger will describe all of the debugging options you have at this point, so feel free to skip ahead to that section or just play around with some of the options on the Debug menu or toolbar. When you are done, you can click the Run or Stop Debugging menu or toolbar option before moving on to the next example.
If you imagine that this script was much larger, you could also imagine that you might not want to use the //x switch to debug the script starting right at the top. Instead, you might only want the debugger to come up if (a) an error occurs, or (b) WSH reaches a manual breakpoint that you have inserted into the code. The //d switch is intended for these two situations.
Let's look at situation (a) first. The script WSH_DEBUG_ERR.VBS contains a divide by zero error. Run this command from the Start ( Run menu (notice that we're using the //d switch now instead of the //x switch).
wscript //d c:scriptsWSH_DEBUG_ERR.VBS
As you can see in Figure 6-9, this time the debugger stops on the line with the divide by zero error. This gives you the programmer the opportunity to use the debugger to observe what happened to cause the error and experiment with ways to fix it. Since we used the //d switch, if this script had not contained any errors, we would have never seen the debugger at all. The //d switch says to WSH 'Only bring up the debugger if I need it,' whereas the //x switch says 'Bring up the debugger no matter what, and break on the first line.'
Figure 6-9
Finally, we can also use the //d switch when we want the debugger to come up on a preset breakpoint. For this purpose, VBScript offers the Stop statement, which is a command to tell the host to activate the debugger, pausing execution on the line with the Stop statement. If no debugger is available, the Stop statement is ignored. For example, if you run a WSH script that contains a Stop statement without using the //d switch, WSH will ignore the Stop statement and will not bring up the debugger.
Setting a manual breakpoint with the Stop statement is useful when you want a script to run up to a certain point where you want to pause execution. If you are debugging a fairly large script, this ability is a huge timesaver, especially if the point from which you want to debug is deep down in the script. The file WSH_ DEBUG_STOP.VBS} contains a Stop statement. If you run it with this command line, you will see that the debugger will come up with a breakpoint on exactly the line with the Stop statement.
wscript //d c:scriptsWSH_DEBUG_STOP.VBS
You've now seen how you can control the activation of a debugging session for WSH scripts in the Script Debugger. The following two sections will explain the same concepts for IE Web page scripts and ASP pages. If you wish to get into the details of how the Script Debugger works once you have started a debugging session, you may wish to skip ahead to the section called Using the Microsoft Script Debugger .
Debugging Client Side Web Scripts with the Microsoft Script Debugger
In this section we will introduce the basics of activating the Microsoft Script Debugger for client-side Web scripts running in IE. We will only explain how to activate the debugger for a script embedded in a Web page opened in IE. Since the usage of the Script Debugger, once activated, is basically the same no matter which host you're running under, usage details are covered separately in a later section called Using the Microsoft Script Debugger . This section also assumes that you have read the previous section called What Is a Debugger? which explains the basic terms and concepts of a debugging tool.
If you have not done so yet, you can download the Script Debugger for free from msdn.microsoft .com/scripting (be sure to get the version that matches your version of Windows). Even though the installation program does not require it, it's a good idea to reboot your machine after installing the Script Debugger.
Enabling the Script Debugger
Once you've downloaded and installed the Script Debugger, the first thing you have to do if you want to debug VBScript in a client-side Web page is to enable the debugging option in IE. Figure 6-10 shows the Advanced tab of the Internet Options dialog box for IE 6. Notice that the option Disable script debugging has been unchecked. This enables the debugger. The next question is how to get it to come up so that you can do some debugging.
Figure 6-10
Activating the Debugger with an Error
Once the Script Debugger is installed and enabled, any time an error occurs in any of the VBScript code embedded in a page, you will have an opportunity to activate the debugger. Take a look at this HTML page, which contains a script with a divide by zero error ( IE_DEBUG_ERR.HTML ).
charset=UTF-8" /> VBScript Programmer's Reference - IE Debugger Example
IE Script Debugger Example: Activate Debugger with Error
This button will call a script that contains a coding error.
If the IE "Disable script debugging" option is turned off, the debugger will active on the line of code with the error.
Test Result: id="txtResult">
Notice that we have a button called cmdTest that activates the script called cmdTest_ OnClick. cmdTest_ OnClick contains a divide by zero error. (If you are not familiar with the basics of embedding VBScript in an HTML page, please consult Chapter 10.) This code will produce the Web page shown in Figure 6-11.
Figure 6-11
If you click the Run Test button, you should see a dialog box like the one shown in Figure 6-12.
Figure 6-12
If you click the Yes button, you should see a window like the one shown in Figure 6-13.
Figure 6-13
As you can see in Figure 6-13, the line with the error is highlighted, with an arrow off to the left pointing to the same line. These indicators mean that the code has paused execution at this point. We now have an active debugging session for this Web page. The section called Using the Microsoft Script Debugger will describe all of the debugging options you have at this point, so feel free to skip ahead to that section or just play around with some of the options on the Debug menu or toolbar. When you are done, you can click the Run or Stop Debugging menu or toolbar option before moving on to the next example.
Activating the Debugger with the Stop Statement
You can also force the activation of the Script Debugger using the Stop statement. Stop is a special VBScript keyword that applies only when script debugging is enabled in the current host (in this case, IE). If no debugger is active, the VBScript runtime engine ignores the Stop statement. (However, particularly for client-side scripts, you don't want to leave any stray Stop statements in your code because your end users might end up looking at a debugger and wondering what the heck is going on.)
Following is a code snippet from another version of the same page from the previous section, this time with the divide by zero error removed and the Stop statement inserted into the code ( IE_DEBUG_STOP .HTML }).
If you click the Run Test button on this page, the Script Debugger will come up automatically, with execution paused on the line with the Stop statement. The Stop statement is one of two ways to set a breakpoint in your code. The Stop statement is useful when you're editing your script and know in advance that you want to set a breakpoint at a certain point in the script. The next section explains how to set a breakpoint once the page is already open in IE.
Activating the Debugger with a Manual Breakpoint
If you already have a Web page with a script open in IE, you can use the View ( Script Debugger (, Open menu in IE to open the debugger before the script starts executing. However, this technique works only if you are debugging a script that is executed 'on demand'-for instance, with the click of a button on the page. If the script is one that runs automatically when the page is loaded, you'll have to use the Stop statement to set a breakpoint.
The page IE_DEBUG_MANUAL.HTML (which, like all examples in this book, is downloadable from wrox.com ) is just like the page examples used in the previous example, except that it does not contain a Stop statement or divide by zero error. If you open this page in IE, you can use the View ( Script Debugger ( Open menu to open the debugger. The debugger window will open, displaying the page with our cmdTest_OnClick procedure.
At this point, you can use the Toggle Breakpoint menu option or toolbar button to set a breakpoint on any line in the script you wish. Then return to the browser and click the Run Test button. The Script Debugger window will come back and paused on the line where you set your breakpoint.
Debugging ASP with the Microsoft Script Debugger
In this section we will introduce the basics of activating the Microsoft Script Debugger for server-side ASP pages running in IIS. We will only explain how to activate the debugger for ASP pages. Since the usage of the Script Debugger, once activated, is basically the same no matter which host you're running under, usage details are covered separately in a later section called Using the Microsoft Script Debugger .
This section also assumes that you have read the earlier section called What Is a Debugger? which explains the basic terms and concepts of a debugging tool. This section also assumes that you are familiar with the basics of administering IIS.
If you have not done so yet, you can download the Script Debugger for free from sdn.microsoft.com/scripting (be sure to get the version that matches your version of Windows). Even though the installation program does not require it, it's a good idea to reboot your machine after installing the Script Debugger.
What about the Visual Interdev debugger? Visual Interdev, an ASP development tool that ships with Microsoft Visual Studio, also includes a debugger that ASP programmers can use to debug ASP Web sites. Programmers who have Visual Interdev available may choose to use that debugger instead of the free Script Debugger. The Visual Interdev debugger does offer some functionality not offered by the Script Debugger, namely the Locals window, watches , and 'Advanced Breakpoints.' Launching the debugger in Interdev is a little different than as described in this section (please consult the Visual Interdev documentation), but once the debugger is activated, the general capabilities and techniques described in this section and the Using the Microsoft Script Debugger section (given later) apply equally well to Visual Interdev and the Script Debugger.
Enabling ASP Debugging
In order for your IIS-hosted Web site to allow debugging of ASP pages, you must explicitly enable this capability. This can be accomplished using the IIS administration tool, which on the latest versions of Windows is available through the 'Administrative Tools' Control Panel applet.
- Right-click on the Web site you wish to debug and choose the Properties menu option.
- Go to the Home Directory tab.
- Click the Configuration button in the lower right corner of the Home Directory tab. This will bring up the Application Configuration dialog box.
- On the Application Configuration dialog box, click the Debugging tab.
- On the Debugging tab, make sure the Enable ASP server-side script debugging option is selected. (You can ignore the Enable ASP client-side script debugging option; it does nothing.)
After enabling this option (if it was not already enabled), it's a good idea to reboot your machine before trying to use the Script Debugger with ASP. Under ideal circumstances, these steps described are all that is required to enable ASP debugging. It is possible, however, that you will have some trouble getting the Script Debugger to work with ASP. Many developers have posted messages in Internet discussion groups attesting to their troubles getting it to work.
If you are having trouble, the Microsoft 'Knowledge Base' ( support.microsoft.com ) does have some articles that may help you. Using the Knowledge Base Search function, search for article IDs 252895, 192011, 244272, 284973, and 312880. If you are still experiencing trouble, try the discussion groups at p2p.wrox.com or communities.microsoft.com .
Finally, be sure to turn off the Enable ASP server-side script debugging option for your Web site before you release the Web site to users. You do not want the debugger to launch when actual users are browsing your Web site. In fact, it's a good idea to perform ASP debugging only on your development machine and never on the actual server hosting your production Web site.
Activating the Debugger
Just as with WSH and IE script debugging, the Script Debugger with ASP pages is activated under two circumstances: when an unhandled runtime error occurs and when IIS encounters a Stop statement in a page's VBScript code.
The previous two sections of this chapter on WSH and IE debugging demonstrate how the debugger activates upon encountering a runtime error, such as a divide by zero error. This works exactly the same with ASP debugging. If ASP debugging is enabled for your site's ASP code and IIS encounters a line of code that causes an unhandled runtime error, then the debugger will come up with a breakpoint on that line. If you want to see this work, modify the asp_debug_stop.asp page (described later) by commenting out the Stop statement and changing the value of x to , which will trigger a divide by zero error.
The downloadable code for this chapter includes a file called asp_debug_stop.asp that is intended to demonstrate how to activate the Script Debugger with ASP using the Stop statement. To try out this example you will need to install the asp_debug_stop.asp file in IIS. Make sure that you install it on a machine that, as described in the previous section, (a) has IIS installed, (b) has the Script Debugger installed, and (c) has at least one running Web site (the 'Default Web Site' will do) with server-side ASP debugging enabled.
This is what the top of asp_debug_stop.asp looks like, before the < HTML > tag.
<%@ Language=VBScript %> <% Option Explicit %> <% Dim strResult Call Main() Sub Main Dim x, y, z Stop x = 5 y = 100 z = y / x strResult = z End Sub %>
Notice the Stop statement inside the Main procedure. As with any other script, this will cause the debugger to pause at this point in the code.
Once you have asp_debug_stop.asp installed under a running Web site on your machine, use IE to browse to that page using http://localhost/ as the beginning of the URL. As soon as you do, the Script Debugger should come up, paused with a breakpoint on the line with the Stop statement. This means that you are now in an active debugging section, with the functionality described in the next section, Using the Microsoft Script Debugger , available to you.
If for some reason the debugger does not come up, but the page does successfully come up in the browser, please consult the previous section, Enabling ASP Debugging , for configuration and troubleshooting tips.
Using the Microsoft Script Debugger
The three previous sections describe how to activate the Script Debugger into a debugging session under the WSH, IE, and IIS. Once you have activated the debugger, the activities you can perform are pretty much the same regardless of which of these hosts you are running under. The following sections will describe the basics of the Script Debugger functionality, including why and when a particular function is useful. This should not be considered comprehensive documentation of the Script Debugger.
The examples in the following sections are based on a WSH script debugging session with the file WSH_DEBUG_EXAMPLE.VBS , which is part of the downloadable code for this chapter. Where necessary, we will point out relevant differences in debugging activities under IE and/or IIS.
If you wish to follow along with the examples, start a debugging session with using the //x command line switch with wscript.exe , as described above in the section called Debugging WSH Scripts with the Microsoft Script Debugger . (If you wish, you could also run it under cscript.exe .) Running the script with the //x switch will activate the debugger, paused at the very first line of code, as shown in Figure 6-14. Remember that, as mentioned before, when debugging a WSH script, the Script Debugger must already be running when you launch the script.
Also, for your reference, here is the code for WSH_DEBUG_EXAMPLE.VBS .
Option Explicit Const FILE_NAME = "WSH_DEBUG_TEST_FILE.TXT" Const COPY_SUFFIX = "_COPY" Const OVERWRITE_FILE = True '***** Main Code Dim objFSO Dim strExtension Dim blnFileExists Dim strNewFileName Dim strScriptPath Set objFSO = CreateObject("Scripting.FileSystemObject") strScriptPath = GetScriptPath() blnFileExists = VerifyFile(strScriptPath, FILE_NAME) If blnFileExists Then strExtension = GetExtension(FILE_NAME) strNewFileName = MakeNewFileName(FILE_NAME, _ strExtension, COPY_SUFFIX) CopyFile strScriptPath & FILE_NAME, _ strScriptPath & strNewFileName, _ OVERWRITE_FILE Else On Error GoTo 0 Err.Raise vbObjectError + 10000, _ "WSH_DEBUG_EXAMPLE.VBS", _ "Expected file " & FILE_NAME & " not found." End If '***** Supporting procedures and functions Private Sub CopyFile(strFileName, strNewFileName, blnOverwrite) objFSO.CopyFile strFileName, strNewFileName, blnOverwrite End Sub Private Function GetExtension(strFileName) GetExtension = objFSO.GetExtensionName(strFileName) End Function Private Function GetScriptPath Dim strPath strPath = objFSO.GetAbsolutePathName(WScript.ScriptFullName) strPath = Left(strPath, _ Len(strPath) - Len(objFSO.GetFileName(strPath))) GetScriptPath = strPath End Function Private Function VerifyFile(strPath, strFileName) VerifyFile = objFSO.FileExists(strPath & strFileName) End Function Private Function MakeNewFileName(strFileName, strExtension, strSuffix) MakeNewFileName = Left(strFileName, Len(strFileName) _ - (1 + Len(strExtension))) & strSuffix & _ "." & strExtension End Function
Figure 6-14
The code is a bit more complex than it needs to be, but that is deliberate in order to create interesting debugging opportunities for the examples given later.
Setting Breakpoints
WSH_DEBUG_EXAMPLE.VBS does not contain any manual breakpoints using the Stop statement. However, since we launched the script using the //x switch, the debugger has set an automatic breakpoint on the first line of the script. You can easily identify the breakpoint since it is highlighted in yellow and has a yellow arrow next to it in the left column of the debugger window.
Now that we are in a debugging session, we can set more breakpoints in the script for this session. To set a breakpoint, use your mouse or your keyboard arrow keys to put the cursor on some other line below the current line. For example, put it on this line.
If blnFileExists Then
Once the cursor is on the line of your choosing, click the Toggle Breakpoint option on the Debug menu or toolbar. Your line should now be highlighted in red with a red circle in the column to the left. The purpose of a breakpoint is to tell the debugger to pause execution on the specified line. This implies that the debugger is running through the code, which is different than your stepping through the code one line at a time (see next section). If you are stepping through the code, breakpoints don't really do anything for you since you are by definition stopping on each line of code.
Therefore, to see your new breakpoint in action, click the Run option on the Debug menu or toolbar. Clicking the Run option is, in effect, telling the debugger to run through the code, only stopping if the debugger encounters one of three things: it reaches a breakpoint like the one we just defined, it reaches an unhanded error, or it reaches the end of the script (in which case the debugging session ends). After you click the Run option, code execution should pause on the breakpoint you defined. The only reason the breakpoint would not work is if you chose a line that is inside of a conditional If Then or Select Case block that does not get executed.
Breakpoints are useful when you want to skip down to examine a deeper part of the script. You could just step down to the line you want to examine, but that could become very tedious in any script that is more than a few lines. A breakpoint, in combination with the Run option, is a quick way to jump down to a particular section of the script.
Stepping through Code
It is important to understand that the yellow-highlighted line of code with the yellow arrow (see Figure 6-14) has not been executed yet . The debugger pauses on a line of code before executing it. Once the debugger is paused on a certain line of code, you have the ability to 'step through' the code. 'Stepping' means executing each line of code one at a time. If you click one of the debugger's 'step' options, the highlighted line of code will be executed and the highlight will move down to the next line.
The Script Debugger provides three different kinds of stepping.
Step Into means that you wish to execute the currently highlighted line of code. The 'into' part of Step Into means that if that line of code calls a procedure or function within the scope of the debugger, the debugger will 'step into' that function, pausing on the first line of code in that procedure or function. (This is in contrast to 'Step Over,' which is described below.)
Notice that we did not say that the debugger will step into every procedure or function-only those that are 'in the scope of the debugger,' which means that the code for the function must be available to the debugger. In general (setting aside the availability of 'symbolic debug info' for external components , which is outside the scope of this book), this means that the procedure or function must be within the same script you are debugging.
Let's look at a Step Into example. To try out the example, start a debug session with the WSH_ DEBUG_EXAMPLE.VBS} script and use the //x option. When the script comes up in the debugger, set a breakpoint on this line of code and click the Run option.
blnFileExists = VerifyFile(FILE_NAME)
The debugger will pause on this line since you set a breakpoint on it. If you now click the Step Into option on the Debug menu or toolbar, the debugger will pause execution on the first line within the VerifyFile function.
Step Over means that you wish to execute the currently highlighted line of code without stepping into any procedures or functions called by that line. For example, if the highlighted line calls a function that you are already familiar and comfortable with, then stepping into that function would be a waste of your time. In this situation, you want to use the Step Over option instead of Step Into .
Going back to the previous example, if you had clicked Step Over instead of Step Into , the debugger would go to the next line after the highlighted line without pausing on any of the lines in the VerifyFile function. Keep in mind that the VerifyFile function is still executed; the difference is that the debugger does not bring you into VerifyFile .
Step Out means that you want the debugger to execute the rest of the current procedure or function without going through it line by line. The debugger will then pause on the next line after the line that called the procedure or function you are stepping out of. In the previous example, if you used Step Into to go into the VerifyFile function, you could use Step Out to complete the execution of the VerifyFile function and then pause again at the line after the VerifyFile call, which is this line.
If blnFileExists Then
The Step Out option is particularly useful when you accidentally click Step Into instead of Step Over . A quick click of Step Out will get you back to where you were as if you had used Step Over instead of Step Into .
Keep in mind that even when using Step Over , if the procedure or function you are stepping over has an unhandled error inside of it, the debugger will pause on the line of code that is about to cause the error. The debugger will always stop for unhandled errors.
Using the Command Window
The Command Window is one of the most powerful features of the Script Debugger. While the debugger is paused on a line of code, you can use the Command Window to view the value of in-scope variables, change the value of those variables, and execute actual VBScript code while the main script is paused. To enable the Command Window , choose the Command Window option on the View menu. Let's look at some examples.
Tip |
If you followed along with the previous example and wish to follow along with the next example, restart WSH _ DEBUG _ EXAMPLE.VBS with a fresh debugging session using the //x option. |
Set a breakpoint on this line in the script and click the Run option.
If blnFileExists Then
This line of code, where we have paused execution with our breakpoint, comes after this line.
blnFileExists = VerifyFile(strScriptPath, FILE_NAME)
This means that at the point we have paused, the value of blnFileExists has already been set by the VerifyFile function. Presuming that everything is set up correctly and WSH_DEBUG_TEST_FILE.TXT is in the same directory as our script, blnFileExists should have a value of True . While the debugger is paused, we can prove this using the Command Window . The Command Window supports a special function using the question mark character (?) that will display the value of any in-scope variable. If you type ? blnFileExists into the Command Window and press the Enter key, the Command Window will display the value of True , as shown in Figure 6-15.
Using the ? operator is one of the most typical things you will do in the Command Window . Together with breakpoints, this is a powerful capability that allows you to see the overall state of your entire script at any point you wish. But viewing the value of a variable is not the only thing you can do in the Command Window .
Suppose we want to test to make sure that the Err.Raise call in the Else block is working as we expect. Making sure that you exercise all of the logic pathways through your code is an important part of testing your scripts before releasing them. Under normal circumstances, the Else block would not be tested unless we renamed or removed the WSH_DEBUG_TEST_FILE.TXT file. However, using the Command Window , we can change the value of blnFileExists to False just to make sure the Else block will be executed at least once. To do this, type blnFileExists = False into the Command Window , just as if you were typing a normal line of VBScript code, and press the Enter key. This operation is shown in Figure 6-16.
Figure 6-15
Figure 6-16
Now we have changed the value of blnFileExists right in the middle of our debugging session. If you click the Run option, the debugger will break on the Err.Raise call (since this amounts to an unhandled runtime error). At this point you can use the Command Window to examine the value of the Err object to see what error is occurring, as shown in Figure 6-17.
This should give you an idea of the power of the Command Window . In addition to doing simple things like changing the value of variables, you can call methods on in-scope objects in your script, and call other procedures and functions within the script. You can even write and execute a mini-script right in the Command Window , as shown in Figure 6-18.
Keep in mind that you can also use the Command Window to access the 'intrinsic objects' available in the VBScript host. The Err object is an example of an intrinsic object that is available in all hosts, but each host has a unique set of intrinsic objects that are available only in that host. For example, WSH has the WScript object. ASP/IIS has the Request and Response objects. These objects, like any other object in scope while the debugger is paused, can be accessed through the Command Window .
Viewing the Call Stack
A call stack is a hierarchical list of the chain of execution within a program. As one function calls another function that calls another function, VBScript is keeping track of this calling order so that as each function completes, VBScript can go backwards 'up the stack.' Sometimes when you are paused inside of a procedure or function you might not be exactly sure how the path of execution got to that point. This is where the Call Stack window in the Script Debugger can help. Let's look at an example.
Figure 6-17
Figure 6-18
Tip |
If you followed along with the previous example and wish to follow along with the next example, restart WSH _ DEBUG _ EXAMPLE.VBS with a fresh debugging session using the //x option. |
Set a breakpoint on this line of code inside of the GetScriptPath function.
strPath = objFSO.GetAbsolutePathName(WScript.ScriptFullName)
Click the Run option, and the debugger will pause on this line of code. Now click the Call Stack option on the View menu. The Call Stack window will come up and should look something like Figure 6-19.
Figure 6-19
The call stack reads from the bottom up (That's why they call it a stack, like a stack of plates or books-first thing in goes on the bottom, last thing in goes on top.). In Figure 6-19, ' VBScript global code ' means that the path of execution started in the 'root' of the script, outside of any named procedure or function. Above that, the line with ' GetScriptPath ' tells us that right now we are in the GetScriptPath function. This simple example does not look like much, but when you have a complex script with many levels of procedures and functions calling each other, the Call Stack window is very helpful for getting your bearings.
Summary
In this chapter, we covered quite a lot of ground. First, we discussed why it is important to care about errors, error handling, and debugging. Second, we covered the different types of errors a script can encounter: syntax errors, runtime errors, and logic errors. Third, we looked at the three primary VBScript hosts (WSH, IE, and IIS) and how errors and error handling are different between them.
Fourth, we introduced the concept of 'error handling,' which means taking a proactive stance as a programmer to ensure that you are testing risky assumptions and that your script will at least fail gracefully if things go horribly wrong. The key points included the use of the Err object, the two On Error statements, which toggle VBScript's default error handling mode, and techniques for logging and displaying errors. For logging and displaying errors, we presented general techniques as well as some that are specific to an ASP application.
Fifth, we discussed the concept of generating custom errors. In addition to explaining how the Err.Raise method can be used to generate your own runtime errors, we went into when it is better to not use Err.Raise and to use a 'problem message' instead. We also introduced strategies for when and how to use Err.Raise .
The second half of the chapter moved away from errors and error handling into debugging. Debugging is the activity of trying to figure out the cause of a problem or error in a script and then taking steps to fix it. One of the most important tools in debugging is called a debugger . A debugger is a tool that allows you to interactively execute a program line by line, including the ability to see the values of variables and even change them. We introduced general debugging terms such as breakpoints, stepping through code, and call stacks.
After this general debugging overview, we explained various debugging techniques that do not involve a debugging tool. Some of these techniques, such as using MsgBox to display the value of a variable at a certain point, are fairly simple. Others are more comprehensive and are ideally built into the infrastructure of your script-based program from the beginning. Even though this section was called Debugging Without a Debugger , many of the techniques are equally useful even if you do have a debugger at your disposal.
To close out the chapter, we covered the freely downloadable Microsoft Script Debugger in great detail. To start with, we explained how the debugger can be used with the three major VBScript hosts (WSH, IE, and IIS). Each host interacts with the debugger differently. In particular, the techniques for enabling the debugger are different from host to host.
After discussing the host-specific Script Debugger details, we concluded with an introduction to the features offered by the Script Debugger that can help you find problems in a script or just understand the code better. These features are the same no matter which host you are using.
Bugs, errors, exceptions, and failed assumptions are inevitable. If you want your programs to have a professional polish, and if you want to make the process of finding and eliminating the inevitable problems to be as short and painless as possible, this is an important chapter to read. A thorough understanding of errors, error handling, and debugging is the mark of a professional programmer.