Day 16

Debugging and Error Handling

by Ted Graham


CONTENTS

One of the greatest benefits of visual design tools, such as IntraBuilder, is that large quantities of bug-free code are generated for you. This is code that you shouldn't have to debug later. However, as you begin to add code, invariably problems arise, some minor and some major. In fact there is probably more effort put into testing and debugging software than into the initial creation of it.

Problems come in several varieties, and some are easier to fix than others. For today's discussion, these problems fall into three categories: planning errors, coding errors, and unavoidable but predictable runtime problems.

Today you will learn about the following topics:

Planning Is A Good Thing

Analyzing the problem and creating a good plan are two essential steps before you start programming. Although few programmers enjoy "programming by committee," certainly many benefits can be gained from "designing by committee." It is vital to ask questions-even outrageous ones-about the problem and the necessary solution. Vital questions are

The more information that is gathered at the beginning, the easier it will be to avoid writing great code that does the wrong thing.

After getting a clear definition of the need, you need to carefully plan the solution to the problem. Although few programmers flowchart their programs like their professors wanted, some level of preparation is always helpful. Actually writing down the program logic flow will help you think through the logic before spending several hours coding, only to discover an easier way.

OOPS Is Not the Plural Form of OOP

Good preparation can prevent many of the logical errors, but only experience can prevent coding errors, especially in such a flexible product as IntraBuilder. Becoming proficient with the syntax of the language might take some time, and during that time you will inevitably use the wrong syntax on occasion. Many of these errors will be caught by the compiler, but others will slip through and only show up when the script produces an error or unexpected result. Programming errors are harder to catch, which is why the software industry keeps so many fine Quality Assurance specialists employed. Only careful coding and thorough testing will flush out these problems. But even if you are new to the JavaScript language, there are steps that you can take to reduce the number and magnitude of programming errors.

Expect the Unexpected

The third type of problem that you will see today is the expected but unusual problem that comes up as scripts run. The real world can present even perfect software with such problems as invalid data or full disk drives. Good software needs to anticipate these problems and prepare to handle them gracefully. This goes back to the planning phase, because part of good planning is to anticipate the unexpected and decide how to handle it.

Building the Perfect Function

To explore some of these problems and ways to solve them, you will write a couple of functions today. The first function will work around a particular oddity found in IntraBuilder's implementation of the JavaScript language. After identifying the problem and charting the course to the solution, you will go through the process of building, testing, and debugging some JavaScript code.

Identifying the Problem

There is an incompatibility between IntraBuilder's version of JavaScript and the implementations in Netscape Navigator and Microsoft Internet Explorer. This difference concerns the conversion of numbers to strings. All implementations of JavaScript allow you to concatenate disparate data types. The data is generally promoted to a string.

NOTE
The automatic conversion of one data type to another is called promotion. This is found in several programming languages, including C++, Java, and JavaScript.

Consider the following JavaScript assignment:


var x = "The answer is: " + 12.345;

alert(x);

The variable x should contain the string value "The answer is: 12.345", and does in most implementations of JavaScript. However, in IntraBuilder, floating point numbers are rounded to a fixed number of decimal places when converted to strings. By default, numbers are rounded to two decimal places. The actual number depends on the configuration of your Windows operating system. Because of this behavior in IntraBuilder, the preceding assignment stores the string "The answer is: 12.35" to x. This rounding occurs only when the number is converted to a string, so it will not affect any calculations; but it can be disconcerting to see the answer rounded like that.

TIP
To set the number of decimal places, select the Regional Settings icon in the Windows Control Panel. There is an option on the Number tab labeled "No. of digits after decimal." In Windows NT 3.51, you should select the International icon in Control Panel and then select the Change button in the Number Format box. This contains an option labeled "Decimal Digits."

Planning the Solution

Suppose that this conversion is the problem that you need to solve in IntraBuilder. So the planning begins. One important step in reducing errors and producing solid code is code reuse. Because you have to solve this problem now, solve it well and solve it in a way that enables you to reuse the solution later. So, rather than solve the problem in the particular form that is displaying the number, create a function that can be used any time that you need to convert a number to a string. As the planning phase continues, you come up with the criteria shown in Figure 16.1.

Figure 16.1 : Functional requirements of the str() function.

Up to this point, the planning has not taken into consideration the actual coding of the function, just the requirements of the function.

Coding the Solution

When you know what the function needs to do, you begin to think through how to code it. It is often easiest to start with a simple specific case and then get more generic. Consider the conversion that identified the problem. How would you produce the string "The answer is: 12.345"? Working through this helps when you are still learning the syntax of the language. The following code creates the desired result:


var num = 12.345;

var sNum = "" + (num * 1000);   // "12345"

sNum = sNum.substring(0,2) + "." + sNum.substring(2,5);

var string = "The answer is: " + sNum;

alert(string);

This code shows the basic approach that you need to solve the more generic problem. You multiply the number until all of the decimal places are to the left of the decimal place. Then when you convert it to a string, all of the significant digits remain visible. Then you insert the decimal place back into the string at the appropriate place.

But how can you tell when all the significant digits are to the left of the decimal place? Again, you can experiment with a small bit of code to find the answer. Try running this script to see what happens:


var num = 12.0000345;

for (var i = 1; i < 30; i++) {

    alert("" + num);

    num *= 10;

}

Two things become apparent from this little exercise. The first is that significant digits can be hidden. In the first couple of alerts, no significant digits are seen to the right of the decimal place. But as the number continues to be multiplied by 10, the other digits appear. The second thing to notice is that at some point the number is displayed in exponential notation. This is the key to finding all the significant digits. Find the largest number before exponential notation kicks in, and you will find the maximum number of significant digits.

Using this basic approach, you can create the generic str() function. Before coding, however, you'll want to diagram at least the basic script flow. Figure 16.2 shows a basic flowchart for this function.

Figure 16.2 : A flowchart for the str() function.

NOTE
There are many flow charting resources available on the Web these days. Use your favorite search engine to look for "flowchart." There are tutorials as well as shareware and evaluation copies of flowcharting software.

Based on this flowchart, you create the script shown in Listing 16.1. The listing contains a few test cases and the str() function. The output of the script is shown after the listing.


Listing 16.1. The JavaScript source for str1.js.

 1: alert("The answer is: " + 12.345);

 2: alert("The answer is: " + str(12.345));

 3: alert("The answer is: " + str(-12.345));

 4: alert("The answer is: " + str(12));

 5: function str (num)

 6: {

 7:     var sNum = "",

 8:           pow   = 0,

 9:           tmp   = "";

10:     if (str.arguments.length == 0)

11:          return ("");

12:     if (num + 0 != num)

13:          return ("" + num);

14:     while ((""+(num*Math.pow(10,pow))).indexOf("E+") < 0)

15:          pow++;

16:     if (pow == 0)

17:          sNum = ("" + num);

18:     else {

19:          var tmp = "" + (num*Math.pow(10,pow-1));

20:          var dec = tmp.indexOf(".");

21:          pow--;

22:          sNum = tmp.substring(0,dec-pow) + "."

23:                    + tmp.substring(dec-pow, dec)

24:                    + tmp.substring(dec+1,tmp.length);

25:          while (sNum.substring(sNum.length-1,sNum.length) == "0")

26:              sNum.string = sNum.substring(0,sNum.length-1);

27:          if (sNum.indexOf(".") == sNum.length - 1)

28:              sNum.string = sNum.substring(0,sNum.length-1);

29:     }

30:     return (sNum);

31: }


This code follows the flowchart in Figure 16.2. Lines 10, 12, and 16 check for the exceptional cases and cause the parameter to be returned using default string conversion. Lines 19 to 28 perform the actual conversion of a numeric parameter to the string. Line 19 converts the number to a string with the maximum number of digits visible. Line 22 inserts the decimal into the string at the correct location, while lines 25 and 27 take care of any trailing zeros or decimal places.

Notice the use of the alert() function in the top of the script file (lines 1 to 4). This function is very useful while writing and testing your scripts, but it should be removed by the time the script is put into production.

TIP
The script in Listing 16.1 assumes that the period is used as the decimal separator (see lines 25, 27, and 32). This might not always be the case. If the server machine has been configured to use different language or regional settings, you should make sure that this script (and others that parse numbers or dates) looks for the correct character.

The script shown in Listing 16.1 compiles fine and produces four alert boxes that show the expected results. Figure 16.3 shows the first of these alert boxes.

Figure 16.3 : The first alert dialog from the str() function.

Debugging Techniques

Now that you are done, you hand the script off to the testers. As testers (and users for that matter) always do, they find several cases where the function returns the incorrect value. The three problems that they find are


alert("The answer is: " + str(.002));

alert("The answer is: " + str(-.002));

alert("The answer is: " + str(0));

In this case, the result shown in the first alert box is The answer is: .2. The second alert box shows the result The answer is: .-2. Neither of these is correct. The third alert seems to hang. Nothing happens. The script keeps running until you press the Esc key and cancel the script.

Now that you have some bugs to work on, the debugging begins. Although many development products come with integrated debuggers, IntraBuilder does not. This makes debugging a little more difficult, but it sure doesn't make it impossible. The real need during debugging (and which debuggers simply make easier) is to track script flow and the values of variables within the script.

Being an Alert Debugger

Because the str(0) problem seems a little more serious, start with that one. To begin debugging, you can again use the alert() function to monitor script flow and the values of variables. Because the script runs continuously, you know that one of the loops is stuck. So you might sprinkle a few alerts around the first while loop:


alert('before first while');

while ((""+(num*Math.pow(10,pow))).indexOf("E+") < 0) {

     alert(""+(num*Math.pow(10,pow)));

     alert("" + (""+(num*Math.pow(10,pow))).indexOf("E+"));

     pow++;

}

When you test str(0), this time you see the following alerts:

You will see more alerts than are shown in this list, because the last two alerts are repeated over and over again. Holding down the Esc key for a while breaks out of the loop. This tells you that the first while loop is in an endless loop, and a closer look at the test expression reveals why. Because num is zero, the calculation always results in zero. This is the standard fare in script debugging. After looking for a way to have the loop handle zero, you decide it's just easier to check for this case along with the parameter checking before the loop. You do this by adding the following lines between lines 13 and 14 in Listing 16.1:


if (num == 0)

         return ("0");

Reusing Debugging Code

Now that you have solved that problem, the alerts are no longer needed. You can delete them, or you can keep them around in case they are needed again in later testing. You can keep the debugging code in place, but not have it execute in the production version by using preprocessor directives.

If Borland's JavaScript is one of your first programming languages, you might not be familiar with the term preprocessor. The preprocessor evaluates the script before it is even compiled. It can substitute values in the script, and it can cause sections of script to be compiled under some circumstances but not others. There is more information about the preprocessor in tomorrow's chapter.

In this case the location alert might be worth keeping around, but the other two are pretty specific to the zero problem. The following listing shows the debug code surrounded with such directives:


#define DEBUG

#ifdef DEBUG

    alert('before first while');

#endif

    while ((""+(num*Math.pow(10,pow))).indexOf("E+") < 0)

         pow++;

The #define DEBUG shown in the preceding code is best placed at the very top of the script file, where it will be easy to find. When you are done testing this script, simply comment out the #define DEBUG command and the debugging code will no longer be compiled into the script.

NOTE
If you have beta tested software, you might have noticed that some beta versions are quite a bit larger than the actual shipping version. The reason is that debug code is often left in place until the product is ready to ship.

Using the Script Pad During Debugging

The next problem to look at is the case of the missing decimal places. If you remember, the tester discovered that str(.002) returns the string ".2", which is missing the leading zeros in the decimal value. Now, some programmer might already have noticed the problem, but like me, many others might initially be stumped by this result.

The alerts are a great help, but they interrupt script flow and can become cumbersome when there are too many of them. Another debugging technique is to output debugging information to the Script Pad window as the script is running.

NOTE
If the Script Pad is not open, you can open it by selecting the View|Script Pad menu option. The Script Pad can be used to test expressions, execute IntraBuilder commands, and view debugging output from scripts.

The Script Pad has an input pane and an output pane. You interact with the output pane by using the _sys.scriptOut object. To see what is in the object, type the following command in the Script Pad input pane:


_sys.inspect(_sys.scriptOut);

This opens the IntraBuilder Inspector so that you can see the properties and methods of the Script Pad output pane. You will probably be using the write(), writeln(), and clear() methods pretty often during debugging, so you should become familiar with them. As an example, type the following commands in the Script Pad:


_sys.scriptOut.writeln("Mary had a little lamb,");

_sys.scriptOut.writeln("Whose fleece was white as snow.");

This should produce the results shown in Figure 16.4. Notice that the commands are collected in the top pane and the results are shown in the bottom pane. You can use the Script Pad to test command syntax. When you have the correct syntax, you can copy and paste the command to your script file.

Figure 16.4 : The writeln() method generates output in the Script Pad window.

The method can be a little cumbersome to call, so you might want to use a shortcut technique that I picked up from a member of the IntraBuilder documentation team: Define a preprocessor macro to call the method. Using this technique, Listing 16.2 shows an updated version of the script that incorporates the fix for zero and some debugging information for the current problem. It also changes the alert() to a writeln() so that it becomes a little less intrusive.


Listing 16.2. The JavaScript source for str2.js.

 1: //alert("The answer is: " + 12.345);

 2: //alert("The answer is: " + str(12.345));

 3: //alert("The answer is: " + str(-12.345));

 4: //alert("The answer is: " + str(12));

 5: //alert("The answer is: " + str(0));

 6: alert("The answer is: " + str(.002));

 7: //alert("The answer is: " + str(-.002));

 8: #define DEBUG

 9: #define OUT(x) _sys.scriptOut.writeln(x)

10: function str (num)

11: {

12:     var sNum = "",

13:           pow   = 0,

14:           tmp   = "";

15:     if (str.arguments.length == 0)

16:          return ("");

17:     if (num + 0 != num)

18:          return ("" + num);

19:     if (num == 0)

20:          return ("0");

21: #ifdef DEBUG

22: OUT('before first while');

23: #endif

24:     while ((""+(num*Math.pow(10,pow))).indexOf("E+") < 0)

25:          pow++;

26: #ifdef DEBUG

27: OUT('pow is ' + pow);

28: #endif

29:     if (pow == 0)

30:          sNum = ("" + num);

31:     else {

32:          var tmp = "" + (num*Math.pow(10,pow-1));

33:          var dec = tmp.indexOf(".");

34:          pow--;

35: #ifdef DEBUG

36: OUT('tmp is ' + tmp + '\ndec is ' + dec + '\npow is ' + pow);

37: #endif

38:          sNum = tmp.substring(0,dec-pow) + "."

39:                    + tmp.substring(dec-pow, dec)

40:                    + tmp.substring(dec+1,tmp.length);

41:          while (sNum.substring(sNum.length-1,sNum.length) == "0")

42:              sNum.string = sNum.substring(0,sNum.length-1);

43:          if (sNum.indexOf(".") == sNum.length - 1)

44:              sNum.string = sNum.substring(0,sNum.length-1);

45:     }

46:     return (sNum);

47: }


Line 8 defines the DEBUG preprocessor constant that causes the debugging code to be compiled into the script. Line 9 defines the preprocessor macro for writing information to the output pane.

Lines 19 and 20 show the fix for zero in its full context. The debugging code in lines 21 to 23 is compiled because of the #define on line 8. If line 8 is commented out, the debugging code is not compiled. The debugging code on lines 35 to 37 should provide the necessary information to solve the current bug. When this updated version of the script is executed, you should get a result like the one shown in Figure 16.5.

Figure 16.5 : Debugging information displayed in the Script Pad window.

Notice the output displayed in the Script Pad window behind the alert box. In particular, notice that the final value of pow is larger than the value of dec. This causes the assignment on line 38 of Listing 16.2 to produce strange results. It seems that having the first significant digit to the right of the decimal throws the assignment off. This assignment can be rewritten to correctly handle the place-holding zeros.


if (dec <= pow)

    sNum = "0." + new StringEx().replicate("0",pow-dec);

else

    sNum = tmp.substring(0,dec-pow) + ".";

sNum += tmp.substring(dec-pow, dec)

            + tmp.substring(dec+1,tmp.length);

This change takes care of the problem with place-holding zeros. All of the test values now produce correct results except for this one:


alert("The answer is: " + str(-.002));

This one still puts the minus sign after the decimal place. Running this test case with the debugging information, you see that it is related to the last problem. The value of dec is less than the value of pow, so the string will start with "0" before adding the value in tmp (which has the minus sign). You could work another check into the part of the code that is building the sNum string to see whether the first character is a minus sign, but it might be easier to simply remove the minus sign before ever starting the conversion. This change has been applied to the code, whose final version is shown in Listing 16.3. Notice that the test code has been moved to a new file named TestStr.js, which is shown in Listing 16.4.


Listing 16.3. The JavaScript source for the final version of str.js.

 1: //#define DEBUG

 2: #define OUT(x) _sys.scriptOut.writeln(x)

 3: function str (num)

 4: {

 5:     var sNum = "",      // string version of number

 6:           pow   = 0,       // keeps track of decimal places

 7:           tmp   = "",      // used while building sNum

 8:           neg   = false   // is num negative

 9:     if (str.arguments.length == 0) // no parameter

10:          return ("");

11:     if (("" + (num + 0)) != ("" + num))   // not a number

12:          return ("" + num);

13:     if (num == 0)

14:          return ("0");

15:     // if negative, set neg flag and make positive

16:     if (num < 0) {

17:          neg = true;

18:          num = -num;

19:     }

20: #ifdef DEBUG

21: OUT('before first while');

22: #endif

23:     // find out the largest number with these digits before

24:     // exponential notation kicks in.

25:     while ((""+(num*Math.pow(10,pow))).indexOf("E+") < 0)

26:          pow++;

27: #ifdef DEBUG

28: OUT('pow is ' + pow);

29: #endif

30:     // simply return the number if it is already in

31:     // exponential notation.

32:     if (pow == 0)

33:          sNum = ("" + num);      // this converts num to a string

34:     // otherwise, build a string with all the digits

35:     else {

36:          // decrement pow by one

37:          pow--;

38:          // create initial string

39:          var tmp = "" + (num*Math.pow(10,pow));

40:          // find current decimal

41:          var dec = tmp.indexOf(".");

42: #ifdef DEBUG

43: OUT('tmp is ' + tmp + '\ndec is ' + dec + '\npow is ' + pow);

44: #endif

45:          if (dec <= pow)

46:              // build place holding zeros

47:              sNum = "0." + new StringEx().replicate("0",pow-dec);

48:          else

49:              // build whole number portion

50:              sNum = tmp.substring(0,dec-pow) + ".";

51:          // now add the rest of the decimal places

52:          sNum += tmp.substring(dec-pow, dec)

53:                      + tmp.substring(dec+1,tmp.length);

54:          // remove trailing spaces

55:          while (sNum.substring(sNum.length-1,sNum.length) == "0")

56:              sNum.string = sNum.substring(0,sNum.length-1);

57:          // remove trailing decimal if there are no decimal places

58:          if (sNum.indexOf(".") == sNum.length - 1)

59:              sNum.string = sNum.substring(0,sNum.length-1);

60:     }

61:     return (neg ? "-" + sNum : sNum);

62: }


For this production version of the script, the #define on line 1 has been commented out. The function no longer displays debugging information in the Script Pad window, and the function should run just a little faster. Also, the pseudo compiled file, str.jo, is a little smaller.


Listing 16.4. The JavaScript source for TestStr.js.

 1: // testing the str() function in Str.js

 2: _sys.scripts.load("str.js");

 3: //

 4: // test zero

 5: //

 6: if (str(0) != "0")

 7:     alert("0 Failed");

 8: //

 9: // test whole numbers

10: //

11: if (str(1) != "1")

12:     alert("1 Failed");

13: if (str(987654321) != "987654321")

14:     alert("987654321 Failed");

15: if (str(-1) != "-1")

16:     alert("-1 Failed");

17: if (str(-987654321) != "-987654321")

18:     alert("-987654321 Failed");

19: //

20: // test decimal values

21: //

22: if (str(.1) != "0.1")

23:     alert(".1 Failed");

24: if (str(.03) != "0.03")

25:     alert(".03 Failed");

26: if (str(12.345) != "12.345")

27:     alert("12.345 Failed");

28: if (str(-.1) != "-0.1")

29:     alert("-.1 Failed");

30: if (str(-.03) != "-0.03")

31:     alert("-.03 Failed");

32: if (str(-12.345) != "-12.345")

33:     alert("-12.345 Failed");

34: //

35: // test bad parameters

36: //

37: if (str("Hi Mom") != "Hi Mom")

38:     alert('"Hi Mom" Failed');

39: if (str(false) != "false")

40:     alert('false Failed');


TestStr.js contains a more exhaustive collection of tests for the str() function. Line 2 loads the function into memory. The first test, on line 6, compares the return value from str() against the expected return value. If the values do not match, line 7 will produce an alert dialog to let you know which test failed. This same pattern is repeated for each of the other tests. Ideally, you should be able to run this test script with no alerts appearing.

Would You Care To Make a Comment?

Something else is different in Listings 16.3 and 16.4. You notice that these files finally have some comments in them. They were left out of the other listings just to save space. No one can ever say enough about the importance of commenting your code. While developing the code, the logic and flow will be obvious to you. After just a few months, however, the code will seem foreign even to you. The better you document the code, the easier it will be to maintain down the road.

Exception Handling

The information above is intended to help you reduce, find, and eliminate logical and coding errors. There is another type of problem that pops up in scripts that you can't eliminate. These problems can and should be prepared for. In object-oriented languages, such as Borland's object-oriented JavaScript, this type of problem is called an exception, and the art of managing the response to exceptions is called exception handling.

What Is an Exception?

The term exception handling seems a bit elusive and esoteric. It sounds like something that the truly geekie discuss at parties to weed out the true nerds from the "wannabes." In object-oriented languages, exceptions are simply objects. And they are very plain little objects at that.

An exception is an object that is created in response to an error condition. It contains properties that define the error that occurred.

To create and inspect one of these simple Exception objects, try running this script:


var x = new Exception();

_sys.inspect(x);

This lets you view the properties of an Exception object. As shown in Figure 16.6, the Exception object contains only three properties: classname, code, and message. The object is simply used to carry information about an error condition. This information can then be used by the IntraBuilder default exception handler or by script designed to handle the exception in some specific way.

Figure 16.6 : An Exception object viewed in the Inspector.

You can create an exception yourself, as shown in the previous code snippet. In addition, an exception is automatically created whenever a runtime error occurs. Try running the following script:


var x = nonexist;

alert("x is equal to " + x);

When this script is run, a runtime error occurs. The error is reported in the dialog box shown in Figure 16.7. In exception-handling terminology, an exception was thrown when the error occurred, and the default system handler caught the exception and handled it by displaying the dialog box.

Figure 16.7 : The dialog box produced by the system exception handler.

Throwing Exceptions

You can take control of any part of this exception-handling process. For example, you throw your own exceptions by using the throw command. In the following script, you generate and throw an exception:


var x = new Exception();

x.message = "An exception has been thrown";

x.code = 0;

throw x;

Just like before, the default IntraBuilder exception handler catches the exception. The dialog that the system displays is shown in Figure 16.8. Notice that it looks a little different for a custom exception than for a default, error-generated exception. IntraBuilder responds better to its own exceptions, providing the option to ignore or fix. However, when the default exception handler responds to your own exceptions, it only reports the message and terminates the script.

Figure 16.8 : The system handler responding to a custom exception.

Catching Exceptions

Not only can you take over the throwing of exceptions, but you can also take over the catching of them. The catching of exceptions is done through the use of a try statement and a catch statement. Each of these statements is followed by a block of code. The try statement is almost always followed by one or more catch statements. The use of multiple catch statements is described a little later. The system-generated exception you saw earlier is caught in the following code:


try {

    var x = nonexist;

}

catch (Exception e) {

    var x = 0;

}

alert("x is equal to " + x);

This code, like the code shown earlier, generates an exception when x is assigned the value of a nonexistent variable. The execution of the try block immediately terminates and script flow continues with the first command in the catch block. The catch block can report the error (using the alert() function or perhaps logging the error in a log file) or can attempt to recover from the error. In this simplistic example, the variable x is simply assigned another value. After the catch block executes and works around the error, script flow continues with the commands following the catch block.

The real power of exception handling is not in catching or throwing system-generated exceptions. The real benefits are derived by the strategic catching and throwing of your own exceptions. As demonstrated earlier, exception handling gives you a chance to recover from problems that can be anticipated.

And finally

As you have already seen, the try and catch statements generally come in sets. This team can be expanded with the inclusion of an optional finally statement, which is added after the last catch block. The finally block is executed after the try or catch block. If no exception is thrown in the try block, the finally block executes immediately after the final command in the try block. If an exception is thrown, the finally block executes immediately after the final command in the catch block that handles the exception.

The script shown in Listing 16.5 demonstrates the use of the finally statement. It also demonstrates the script flow associated with try, catch, and finally statements.


Listing 16.5. The JavaScript source for finally.js.

 1: var f=new File();

 2: try {

 3:     try {

 4:          f.open("test.txt","A");

 5:     }

 6:     catch (Exception e) {

 7:          var x = new Exception();

 8:          x.message = "Can not open test.txt";

 9:          x.code = 0;

10:          throw x;

11:     }

12:     f.writeln("Hi Mom");

13: }

14: catch (Exception e) {

15:     _sys.scriptOut.writeln("Exception: " + e.code);

16:     if (e.code != 0)

17:          throw e;

18: }

19: finally {

20:     f.close();

21: }


This script is supposed to open a text file for output and then write the string "Hi Mom" to the file. The file is opened using the File object created on line 1. The first try block, lines 2 to 13, surrounds the code that opens (line 4) and writes to (line 12) the file. Any exceptions thrown in this block are caught by the catch block on lines 14 to 18. The open() method call on line 4 is also surrounded by a second try block. If the file cannot be opened, a system exception is thrown, which is caught by the catch block on lines 6 to 11. This then throws a custom exception to the outer catch block, essentially replacing the system exception with a custom one.

Assuming that test.txt exists (which it probably will not), no exceptions are likely and only four lines of code are executed: 1, 4, 12, and 20.

If the test.txt file does not exist, line 4 generates an exception, causing the lines of code in the inner catch block to execute. When line 10 executes, script flow passes to the outer catch block, so that line 12 is never executed. Line 15 writes a note about the exception to the Script Pad window. Then line 20 is executed.

Notice that the finally block is always executed. This makes sure that the file is closed, regardless of whether an error occurs. In the most likely scenario, the file is never opened; however, it is possible that the file could be opened, but then line 12 could fail. Any failure in writing to the file would be re-thrown by line 17. In this case, line 20 would be necessary to make sure that the file was not left open.

Subclassing Exceptions

So far, you have only thrown and caught objects of class Exception. Like all other IntraBuilder classes, you can create custom classes that are based on this base class. This is a common technique used to throw custom exceptions. Rather than create a standard exception with a specialized message and code, you can simply create a subclass of the Exception class. Then you can use multiple catch statements, each one catching a particular Exception class.

TIP
When you have multiple catch statements after a try statement, each catch statement is checked in the same order in which it occurs in the code. The first catch statement that catches the thrown Exception class or any subclass of the thrown exception handles the exception. All other catch statements are ignored.

The script from Listing 16.5 has been rewritten in Listing 16.6 to use a custom exception class for the open error.


Listing 16.6. The JavaScript source for finally2.js.

 1: var f=new File();

 2: try {

 3:     try {

 4:          f.open("test.txt","A");

 5:     }

 6:     catch (Exception e) {

 7:          throw new FileOpenException();

 8:     }

 9:     f.writeln("Hi Mom");

10: }

11: catch (FileOpenException e) {

12:     _sys.scriptOut.writeln("Can not open file");

13: }

14: catch (Exception e) {

15:     throw e;

16: }

17: finally {

18:     f.close();

19: }

20: class FileOpenException extends Exception {}


The custom exception class is defined on line 20 and is used on line 7. Notice how much more succinct line 7 is compared to lines 7 to 10 in Listing 16.5. Using custom exception classes really pays off when you will be catching and throwing many of your own exceptions.

Notice that this version of the script has two catch statements following the outer try statement. If the inner catch block throws a FileOpenException, the catch on line 11 handles it. If any other exception is thrown, the catch on line 14 handles it.

NOTE
The catch block that starts on line 14 is there to demonstrate the principle. In this case, it could have been left out because the exception would have been passed on to the system handler anyway.

The DbException Class

In addition to the built-in Exception class, there is one custom exception class also built into IntraBuilder. The DbException class is thrown whenever an error is generated by the Borland Database Engine (BDE). In addition to the message and code properties, the DbException class contains a property named errors. This is an array of DbError objects. When a database error occurs, there could be one or more errors generated by the BDE. Each of these errors produces a DbError object that is stored as an element in the errors array.

The code in Listing 16.7 demonstrates catching DbException objects and then displaying each of the database errors.


Listing 16.7. The JavaScript source for dbexcept.js.

 1: try {

 2:     q=new Query();

 3:     q.sql = "select * from nonexist";

 4:     q.active = true;

 5: }

 6: catch (DbException e) {

 7:     for (var i = 0; i < e.errors.length; i++)

 8:          alert(e.errors[i].message + " (" +

 9:                   e.errors[i].context + ")");

10: }

11: catch (Exception e) {

12:     alert(e.message);

13: }


The SQL statement on line 3 is very likely to cause an error (unless you happen to have a table named nonexist laying around). The actual error will occur when line 4 executes. This error causes a system exception to be thrown, which is caught by the catch on line 6. This catch block then displays an alert dialog for each of the database errors.

It is worth mentioning again that the order of multiple catch statements is significant. The catches are evaluated in the same order in which they appear in the script. Only one catch statement executes. A catch statement executes if it catches the thrown exception or any subclass of the thrown exception. If the two catch statements on lines 6 and 11 were reversed, the DbException one would never execute because the Exception one would catch every exception, due to the fact that all custom exceptions are subclassed from Exception.

Document the Exceptions

This final point about exceptions is very important. If you write a reusable component, such as a function or class, be sure to document any exceptions that might be thrown by the class. This allows those who use the class (which might be you in six months) to know what exceptions are thrown and to anticipate those exceptions.

You should document any system-generated exceptions that you anticipate being thrown from the component. For instance, if you create a component that accesses table data and you don't catch possible exceptions yourself, the calling script has to prepare for possible exceptions and try to handle any failure gracefully.

More importantly, you should document any custom exception classes that you throw from the component. For instance, consider a class that is used to generate log files. Such a class is shown in Listing 16.8. This script demonstrates many of the exception-handling principles that were covered today. In particular, it documents the exception classes that are thrown. These exception classes are defined right before the LogFile class itself.


Listing 16.8. The JavaScript source for logfile.js.

  1: // LogFile class helps keep activity logs

  2: //

  3: // NOTE: this class throws exceptions:

  4: //

  5: //    LfCanNotCreate - thrown from constructor if the log

  6: //                              file does not exist and cannot be

  7: //                              created. This exception is thrown

  8: //                              regardless of the throwExceptions

  9: //                              property value.

 10: //    LfCanNotOpen    - thrown from methods if the log file

 11: //                              cannot be opened. The log entry is

 12: //                              cached and will be written the next

 13: //                              time the file is opened.

 14: //    LfInvalidLogName - thrown from methods if the log file

 15: //                                 name used to create the object was

 16: //                                 invalid. The constructor would also

 17: //                                 have thrown the LfCanNotCreate

 18: //                                 exception.

 19: //

 20: // Usage:

 21: //

 22: //      new LogFile(<logName>)

 23: //

 24: //      where <logName> is a valid file name. If the file does

 25: //                              not exist, it will be created. Any

 26: //                              directories in the path must already

 27: //                              exist. If an existing file name is

 28: //                              used, the new log entries will be

 29: //                              appended to the end of the current file.

 30: //

 31: // Properties:

 32: //

 33: //    cacheSize - If a log entry cannot be written to the log

 34: //                      file, it is cached. This is the maximum

 35: //                      number of entries that can be cached. The

 36: //                      next time the log is successfully opened, the

 37: //                      cache is flushed.

 38: //    retry       - The number of times that the object attempts

 39: //                      to open the log file.

 40: //    throwExceptions - Determines if internal exceptions are

 41: //                               thrown out of the methods. If an

 42: //                               exception is thrown during a method,

 43: //                               the operation is simply aborted. To

 44: //                               notify the calling script, set this

 45: //                               property to true.

 46: //

 47: // Methods:

 48: //

 49: //      flush()         - Flushes the current contents of the

 50: //                            cache, if any.

 51: //      log(<entry>) - Writes the <entry> string to the log

 52: //                            file. If the log file cannot be opened,

 53: //                            up to cacheSize entries will be stored

 54: //                            in memory. The contents of the cache are

 55: //                            flushed the next time the log file is

 56: //                            successfully opened (with a call to the

 57: //                            flush() or log() method).

 58: //

 59: // Example:

 60: //

 61: //      try {

 62: //          var log = new LogFile("test.log");

 63: //          log.log("Start Operation");

 64: //      }

 65: //      catch (Exception e) { // just ignore any errors

 66: //      }

 67: //      var x = DoSomething();

 68: //      try {

 69: //          log.log("Results of DoSomething: " + x);

 70: //          log.log("End Operation");

 71: //      }

 72: //      catch (Exception e) { // just ignore any errors

 73: //      }

 74: //

 75: //

 76: // Define the exception classes used by the LogFile class

 77: //

 78: class LfCanNotCreate    extends Exception {}

 79: class LfCanNotOpen       extends Exception {}

 80: class LfInvalidLogName extends Exception {}

 81: //

 82: // Define the LogFile class.

 83: //

 84: class LogFile(logName) extends File

 85: {

 86:     // set the property values

 87:     this.cacheArray = new Array(); // cache unwritable entries

 88:     this.cacheSize   = 10;               // size of cache

 89:     this.logName      = logName;       // name of logfile

 90:     this.retry         = 10;               // open retry limit

 91:     this.throwExceptions = false;   // throw exceptions out of

 92:                                                    // class methods

 93:     //

 94:     // This try block is used to surround code for

 95:     // creating the log file. If any errors occur

 96:     // the following catch will respond to them.

 97:     //

 98:     try {

 99:          // if the file does not exist, then create it

100:          if (!this.exists(this.logName)) {

101:              this.create(this.logName)

102:              this.writeln("Log File Created");

103:          }

104:     }

105:     //

106:     // Catch any errors that occur. If there is an error

107:     // override the open method (so the object doesn't

108:     // try to open a file that wasn't created) and then

109:     // throw an LfCanNotCreate exception to calling

110:     // script.

111:     //

112:     catch (Exception e) {

113:          this.open = {;throw new LfInvalidLogName()};

114:          throw new LfCanNotCreate();

115:     }

116:     //

117:     // The finally always happens. This is used to make

118:     // sure that the file is closed, even if an error

119:     // occurred.

120:     //

121:     finally {

122:          this.close();

123:     }

124:     function cache(logEntry)

125:     // This method stores an entry to the object's cache, if

126:     // the cache is not already full. After the cache is full,

127:     // new entries are lost.

128:     {

129:          if (this.cacheArray.length < this.cacheSize)

130:              this.cacheArray.add(logEntry);

131:     }

132:     function flush()

133:     // This method attempts to flush the cached entries to the

134:     // log file. The try block surrounds commands that may

135:     // produce exceptions. The catch then throws the exception

136:     // if the throwException property is true. The finally makes

137:     // sure that the file gets closed, whether or not an error

138:     // occurs.

139:     {

140:          try {

141:              this.open();

142:              this.flushCache();

143:          }

144:          catch (Exception e) {

145:              if (this.throwExceptions)

146:                   throw e;

147:          }

148:          finally {

149:              this.close();

150:          }

151:     }

152:     function flushCache()

153:     // This method writes each cache entry to the open

154:     // log file. The open() method should be called before

155:     // calling the flushCache() method.

156:     {

157:          for (var i = 0; i < this.cacheArray.length; i++)

158:              this.writeln(this.cacheArray[i]);

159:          this.cacheArray = new Array();

160:     }

161:     function log(logEntry)

162:     // This method writes an entry to the log file. The entry

163:     // can be of any data type.

164:     {

165:          try {

166:              this.open();

167:              this.writeln("" + logEntry);

168:          }

169:          // The first catch block catches only the LfCanNotOpen

170:          // exceptions, which may be thrown by the open() method.

171:          // When this exception occurs, the entry is cached.

172:          catch (LfCanNotOpen e) {

173:              this.cache(logEntry);

174:              if (this.throwExceptions)

175:                   throw e;

176:          }

177:          // The second catch block catches any other exceptions

178:          // that may occur. This one simply throws the exception

179:          // to the calling script if throwExceptions is true.

180:          catch (Exception e) {

181:              if (this.throwExceptions)

182:                   throw e;

183:          }

184:          // The finally block makes sure that the log file is

185:          // closed, whether or not an error occurs.

186:          finally {

187:              this.close();

188:          }

189:     }

190:     function open()

191:     // This open() method overrides the built in open()

192:     // method of the file object. It makes multiple attempts

193:     // to open the log files, based on the retry property.

194:     // Once the log file is open, it flushes the cache.

195:     {

196:          var isOpen = false;

197:          var tries = 0;

198:          // make multiple attempts to open the file

199:          while (!isOpen && tries < this.retry) {

200:              // if the file cannot be opened, an exception is

201:              // thrown.

202:              try {

203:                   // notice the use of the scope resolution operator

204:                   // (::) to call the original open() method of the

205:                   // base class, File.

206:                   isOpen = File::open(this.logName,"A");

207:              }

208:              // any errors are simply ignored

209:              catch (Exception e) {

210:              }

211:              tries++

212:          }

213:          // if the file was not opened, throw an LfCanNotOpen

214:          // exception. This passes control out of the method

215:          // before the call to flushCache can occur.

216:          if (!isOpen)

217:              throw new LfCanNotOpen();

218:          // flush the cache if necessary

219:          if (this.cacheArray.length > 0)

220:              this.flushCache();

221:          // if no exception was thrown, then everything must

222:          // have been successful, so return true.

223:          return (true);

224:     }

225:     function writeln(logEntry)

226:     // This method overrides the writeln method in the base

227:     // class. It writes the date and time before writing

228:     // the actual entry. Notice the use of File:: to refer

229:     // to the methods of the base class.

230:     {

231:          return File::write(new Date() + " ") +

232:                    File::writeln(logEntry);

233:     }

234: }


This production script contains the necessary documentation on lines 3 to 18 so that users can prepare to catch the custom exception classes. These custom classes are defined on lines 78 to 80. The exceptions are thrown on lines 113, 114, and 217.

Take a minute to look at line 113. This line makes use of the dynamic object model by overriding the open() method if there were any errors creating the log file. If the log file could not be created, any calls to the open() method automatically throw an exception.

Summary

Today you learned about debugging scripts and handling runtime errors that can be anticipated. The most important step in eliminating logical and coding errors is thorough planning. After coding and testing, you can use alerts and output to the script pad or a log file to track program flow and the values of variables.

Iron-clad script also needs to anticipate problems that can occur at runtime. Exception handling allows you to catch these errors and gives you the opportunity to respond gracefully.

Q&A

Q:Can the planning period really prepare for all the possible problems that will arise during the development cycle?
A:Thorough planning can prepare you for many of the conditions and problems that may come up during development, but there might be situations that the planners don't anticipate. There are certainly trade-offs involved here. Planning time takes away from actual coding time and needs to be wrapped up at some point. If the planning period lasts too long, the benefits of the additional planning time might be outweighed by the lost coding and testing time. But too little planning or too little input from developers, testers, and users can end up crippling the development schedule as unforeseen problems are discovered, debugged, and fixed.
Q:Can you write to the Script Pad window in a production application?
A:You can write to the Script Pad window, but it does little good because the Script Pad window is not visible in the IntraBuilder Server. On the other hand, it does little harm, except causing the script to run a little slower. If you need to track information in a production application, consider writing it to a log file or storing information in a table.
Q:Is it better to throw an exception or to simply handle a problem right when it occurs?
A:In many cases, the place where you need to respond to an error condition is not the same place that the error actually occurs. In these cases, throwing an exception back to the calling script allows the error recovery to take the context into consideration.
Q:If an exception is thrown within a try block, does the catch statement that follows that try block have to handle the exception?
A:No. If none of the catch statements following that try statement catch that Exception class (or one of its base classes), then the exception will be thrown to any other try/catch block in the call stack. If there are no more try/catch blocks in the call stack, the system exception handler responds to the exception.

Workshop

The Workshop section provides questions and exercises to help you get a better feel for the material you learned today. Try to answer the questions and at least think about the exercises before moving on to tomorrow's lesson. You'll find the answers to the questions in Appendix A, "Answers to Quiz Questions."

Quiz

  1. List three uses of the Script Pad window during script development.
  2. When can you use an alert() function, and when should you not use it?
  3. The following script seems to have a problem with the sql statement that is generated. How can alert() or _sys.scriptOut.writeln() be used to determine what the problem is? (To test this script, create the .JS file in the IntraBuilder\Samples directory.)
    alert("Total orders for 1221: $" + totalOrder('1221'));
    function totalOrder(custNum) {
    var sql = 'select sum(total) as totalOrder'
    sql += 'from orders.dbf o ' ;
    sql += 'where o."Customer_n" = '
    sql += ("'" + custNum + "'")
    q = new Query();
    q.sql = sql;
    q.active = true;
    return (q.rowset.fields["totalOrder"].value);
    }
  4. What is an exception?
  5. Does a try statement always need a catch statement?

Exercises

  1. Write a function, DebugAlert(), that takes a single parameter and then displays the parameter in an alert dialog. Use a preprocessor directive so that the alert only occurs when in debug mode.
  2. Write a script that clears the Script Pad output pane and then displays your name and address in the output pane. The output should match the format that you commonly use when addressing an envelope.
  3. There are at least two things wrong with the following code. What are they? Assume that the DoSomething() function is defined elsewhere in the same script and that it may or may not throw an exception.
    try {
    var x = "tmp" + parseInt(Math.random()*10000);
    _sys.databases[0].copyTable("orders.dbf",x);
    DoSomething(x);
    _sys.databases[0].dropTable(x);
    }
    catch (Exception e) {
    _sys.scriptOut.writeln("An error occured.");
    }
    catch (DbException e) {
    _sys.scriptOut.writeln("A database error occured.");
    }
  4. Modify the script in exercise 3 to correct both of the problems that you found. Test with all three versions of DoSomething() shown in the following code:
    function DoSomething(){
    throw new Exception();
    }
    function DoSomething(){
    throw new DbException();
    }
    function DoSomething(){
    return true;
    }