
Strategies for Debugging PDF JavaScript
Hunt bugs down and zap them!
Ordinary mortals need only worry about two of life's certainties: death
and taxes. If you're a programmer, you worry about three things: death,
taxes and bugs.
In an earlier column,
I suggested strategies for avoiding bugs in your
JavaScript code. But avoiding bugs and finding them are two different
tasks. Sooner or later, you're going to have to go on an expedition to
locate the source of a problem in your code. That's when the real fun
begins.
Heed the Error Message
Often, the nature of the bug will be apparent from the error message
that you get on the JavaScript console. Consider the following examples:
- "JavaScript error in [something] script of field '[whatever]'
at line [number]": This message points you to the bug location in a script
attached to a form field, providing the script is self-contained. If it
hands control to a document-level (or other) script, the bug may actually
be in the externally referenced function rather than in the field script.
- "Unterminated string literal": Means you probably broke a
string across two or more lines (using a carriage return) without using a
continuation backslash at the end of the broken line(s).
- "Missing ; before statement": Can mean (among other things)
that you nested too many quotation marks in a string, causing premature
string termination.
- "NaN": Not a number. Means you probably tried to do a numeric
operation on something that couldn't be converted to a number.
- "Invalid or unknown property, set not possible": Means you
tried to do something like this.getField('abc').value = 99.9, but
there is no field with the name 'abc'.
- "[variable name] has no properties": Means you used a dot
operator on something that was not an Object.
- "[variable name] is not defined": Means you tried to manipulate
a variable that was never declared.
If you're lucky, most bugs of the above kind(s) will be due to simple
typing mistakes; debugging will come down to finding (and eliminating) a
stray quotation mark or carriage return. But that won't always be the case.
Developing Good Intuition
What about errors that don't generate any runtime console messages? Your
first indication of a problem may simply be that your program doesn't take
the desired action on a field value or fails, in some way, to do what it's
supposed to do. (In rare cases, you may actually lock up your computer or
cause Acrobat to crash.) Without any console error messages to guide you,
how are you supposed to locate the source of a hidden problem?
This is where debugging becomes as much of an art as a science. With
experience, you'll come to develop a pretty good sense of intuition as to
where a given type of bug in your code is most likely to be found. Of
course, having good intuition is partly a matter of knowing how to "factor
out" your code into short, well-defined functions, so that bugs are more
likely to reveal themselves in terms of functionality. (Which would you
rather do: Find a bug in a 100-line function, or find a bug in a 5-line
function?) This, in itself, suggests a useful heuristic: Divide a buggy
function up into shorter functions and see if the bug goes away. If it's
still present, at least it should be easier to locate.
The Binary-Search Technique
I like to think of the debugging process as a matter of forcing the
computer to show me what's wrong with my code. I like to make
the computer point me to the exact spot in my code where things start going
haywire. One way of doing this is by making the program show you (in an
alert dialog) key variable values at various breakpoints in the code. The
app.alert() method is great for this, since it interrupts program
flow at whatever point you insert it, and doesn't return control to the
program until you dismiss the alert dialog. Even more convenient is the
fact that JavaScript tries to convert whatever you put as an argument to
app.alert() into a String.
But it can be inconvenient, when you're dealing with large amounts of
code and don't have the slightest idea where a bug is, to insert
app.alert() statements throughout your code in a million different
places. Fortunately, there's an easy way out: the binary search technique.
The binary search method goes something like this. Suppose you have a
complicated function that's 125 lines long (yeeouch!) and you suspect the
bug is lurking somewhere in that dense tangle of expressions. First, insert
app.alert() at a breakpoint that's near the middle of the code. Make
the argument to app.alert() a key variable whose value will tell you
unambiguously whether execution has proceeded normally (or not) up to that
point. Run the code and see what the variable's value is at the breakpoint.
If the value is defective, take out app.alert() and reinsert it at a
point that's midway between the beginning of the function and the last
place where you used app.alert(). (If the variable's value was
normal, insert app.alert() midway between the end of the
function and the last breakpoint.) Run the code again. Repeat the process
until you've found the bug location.
The idea is that each time you establish a new breakpoint, you partition
the code in such a way as to narrow down the location of the bug, ruling
out 50% of your code as possible bug territory each time. In this fashion,
you can zero-in on a bug's location in log-N attempts, where N is the
number of lines of code and "log" means the base-2 logarithm. In the
example above, it should take no more than 7 tries to find a bug in 125
lines of code. (The base-2 log of 125 is six-point-something.) That's much
better than moving the breakpoint a few lines at a time and hoping for the
best...a process that could take all day.
Intermittent Errors
Bugs that show up intermittently can be extremely difficult to find, for
obvious reasons. The first order of business is usually to find a reliable
way to replicate the error. Often that can be done by substituting a
blatantly extreme value for a key variable. (Hint: Some good values to try
are zero, 'null', and 'undefined'.)
Frequently, when something goes awry on an intermittent basis, it's
because a variable acquired a value that's either downright nonsensical (or
undefined) or outside some minimum or maximum limit. For example, if you
have a routine that relies on taking the square root of a value, and that
value turns out, on occasion, to be a negative number, you've got a problem
of the 'NaN' variety. An even more common situation is one in which one
variable is divided by another, but no check is ever done as to whether the
denominator might be zero. (Dividing by zero is a no-no. It gives rise, in
JavaScript, to the value Number.POSITIVE_INFINITY or
Number.NEGATIVE_INFINITY.) But the situation that gives rise to the number
error may be something that only happens under very special conditions;
hence the intermittent-ness
As a rule, if you're experiencing a bug that's intermittent, you should
start thinking in terms of key variables having taken on on pathological
values under "boundary conditions." Look through your code to see if there
are any places where you should be doing "sanity checks" on variables. If
you have variables whose values must fall between set limits at
runtime, write a routine called Clamp() that simply enforces a bounds-check
on any passed-in variable:
function Clamp(n,lowlimit,highlimit) {
if (n < lowlimit) return lowlimit;
if (n > highlimit) return highlimit;
return n;
}
At the beginning of every routine, do a sanity check on incoming
parameters (arguments). If it doesn't make sense to continue execution when
an argument has a null value or is equal to zero, check for this
possibility and return control to the caller as necessary. Don't assume
that incoming parameters have logical values! In fact, in JavaScript, you
shouldn't even assume that incoming parameters have a particular
type.
Misbehaving Operands
JavaScript is a loosely typed language, meaning that variables can
behave like Strings or like Numbers depending on context; the interpreter
will do automate type conversion to fit the circumstances, whether
that's what you intended or not. So when variables are misbehaving,
always consider the possibility that what you thought was a Number is
actually being treated like a String, or vice versa. Remember that the plus
sign is not just an arithmetic addition operator; it is also a
string-catenation operator. Under some conditions, the interpreter, on
encountering a plus sign, may not know whether you're trying to do
string-catenation or addition. And it may pick the wrong thing to do.
The best rule of thumb is, when it's critical that a variable behave as
a Number, explicitly cast it to a Number. When it's critical that a
variable behave as a String, cast it to a String:
var subtotal = Number( userValue1 ) + Number( userValue2 );
var bigString = String( userValue1 ) + String( userValue2 );
Sometimes It's Not Your Code That's Buggy
If you spend enough time programming (and debugging), you'll eventually
encounter a situation where the machine's misbehavior is tied not to your
code, but to Acrobat's (or even more rarely, to the JavaScript interpreter
itself). For example, on the Mac version of Acrobat, the
charCodeAt() method of JavaScript's String class reports negative
values for high-bit ASCII characters. (I haven't checked whether this is
also the case on the Windows version.) This is not in keeping with the
Netscape implementation of JavaScript, where character codes are always
positive, in the range zero to 255. The Netscape implementation is correct.
The implementation in Acrobat is not. I discovered this bug when I was
using the return value of charCodeAt() to index into a table (array)
in a JavaScript routine I was using for encryption. Using a negative number
as an array index is an automatic bug in every language I know of, although
it will get past the compiler since there is no syntax error.
The Machine Must Do What You Tell It to Do
When a debugging situation arises where you're absolutely convinced that
you've entered some kind of metaphysical reality warp, because you KNOW the
code can't be doing what it's doing, and you're ready to pull your hair out
because there is NO WAY the code you've written could be incorrect, yet the
program is misbehaving, sit back and take a deep breath, and remind
yourself that it's just a machine; it can only do what you tell it to do.
If there's a bug, you must find a way to tell the machine to reveal it to
you. Write "monitors" into your code so you can observe variable values at
runtime. Set up breakpoints at critical spots. Make the machine
prove to you that your code is incorrect, and make it show
you where the defect is. If you're a good programmer, that should be a
straightforward programming task, like any other.