The JavaScript Apostle
Advanced JavaScript Debugging Techniques
By Danny Goodman
Send comments and questions about this article to View Source.
Click here for printer-friendly version
The prerelease version of Netscape's JavaScript
Debugger component for Netscape Communicator has been one of the most welcome
hunks o' code to reach my desktop in a long while. In his forthcoming article
on the JavaScript Debugger, Angus Davis presents a good overview of how
to use the debugger in your client-side development. Still, there are many
occasions when I fall back on some of my older debugging techniques to
give me a different perspective on what's happening inside my code.
In this article, I'll provide you with a simple
but powerful external .js library file that preserves a trace
of designated values while a script runs. You specify which values are
to be monitored and where in the code this happens. As an added bonus,
this library file also measures time intervals between tracking statements
-- all the better to help you identify where performance bottlenecks are
slowing down your program. If you're still authoring in Navigator 3, you
can use this debugging method in lieu of the Communicator-level debugger.
Some of the techniques here can also apply to server-side JavaScript.
BUG PATROL
I've long held that 90 percent of the bugs in
JavaScript code are related to unexpected expression evaluation and faulty
object references. At times you think that an expression has one type of
data in it, when, in truth, something entirely different is there (a benefit
and penalty for using a loosely typed language); object references can
also go awry if you accidentally duplicate object names or overlook the
nesting of objects in their containers (for example, a text box inside
a form inside a document).
Single-stepping through running code is an extremely
valuable debugging aid when you know where the problem is. But often your
application has grown so large that you may be able to debug it more efficiently
by seeing a trace of execution with intermediate values along the way.
For example, you'd like answers to these kinds of questions:
-
How many times is that loop executing?
-
What are the values being retrieved each time through
the loop?
-
Why won't the while loop exit?
-
Are comparison operators behaving as I'd planned
in if-else constructions?
-
What kind of value is a function returning?
With thoughtfully placed calls to my external library's
debug() method, the report you get after running your script can answer
questions like these and many more.
DEBUG.JS
Example 1 is the listing for the debug.js
library file. It consists of one global variable and one function.
Example 1
var timestamp = 0
function debug(flag, label, value) {
if (flag) {
var funcName = debug.caller.toString()
funcName = funcName.substring(10, funcName.indexOf(")") + 1)
var msg = "In " + funcName + ": " + label + "=" + value
var now = new Date()
var elapsed = now - timestamp
if (elapsed < 10000) {
msg += " (" + elapsed + " msec)"
}
timestamp = now
java.lang.System.out.println(msg)
}
}
When this file loads into a document, the
timestamp variable becomes a global variable in the document. This
variable is used to store the last time the debug() function is
called. The value must persist so that subsequent calls to debug()
permit calculations of the time differences between the previous and
current invocations.
The debug() function takes three parameters.
The first, flag, is a Boolean value. Later in this article, you'll
see how flag comes into play with the calls to this function.
The second parameter is a string that identifies, in plain language, the
value being traced. And the value itself is passed as the third parameter.
Virtually any type of value or expression can be passed as the third parameter
-- which is precisely what you want in a debugging aid.
Only if the flag parameter is true
does the bulk of the debug() function execute. The first
task is to extract the name of the function from which debug() was
called. By retrieving the rarely used caller property, debug()
grabs a string copy of the entire calling function. A quick extraction
of a substring from the first line yields the name of the function. The
second task is to start building the message text that records this particular
trace. The message identifies the calling function followed by a colon;
after that comes the label text passed as the second parameter plus an
equal sign and the value parameter. Here is the format of the
output message:
In funcName: label=value
By itself, the message contains a fair amount of
information. But I take it one step further by calculating the current
time from the system clock. By subtracting the value of the timestamp
variable from the current time, I get an elapsed time. If that elapsed
time is less than ten seconds, I append the elapsed time (in milliseconds)
in parentheses to the message. Then I update the timestamp global
variable.
The final statement is a LiveConnect call to a
native Java class method. Experienced Java programmers will recognize the
System.out.println() method as the one that writes a value to the
Java Console window. If you haven't done any Java programming, you may
not even know that Navigator has a Java Console window available in the
Options (or Communicator) menu. Java applet errors are automatically written
to this window, even if the window is hidden. Similarly, you can use the
LiveConnect direct call to a built-in Java method, java.lang.System.out.println(),
to write anything you like to the window. Anything written to that window
is appended to the end of whatever text is already inside. Therefore, you
can write as many debug messages as you like to that window, and they'll
all be there for you to see after the script runs.
LOADING DEBUG( )
To include this debugging library in your document,
add the following <SCRIPT> tag set at the beginning of the
document's head section:
<SCRIPT LANGUAGE="JavaScript" SRC="/old?u=http%3A%2F%2Fdeveloper.iplanet.com%2Fviewsource%2Fdebug.js&y=1999"></SCRIPT>
The syntax assumes that you have saved debug.js
in the same directory as your HTML document. Also, if you're running
the page from a server (instead of a local hard disk), the server must
be configured to transmit files with the .js extension as the
MIME type "application/x-javascript". Netscape servers are set up to do
this by default. If you run the page from a local hard disk, no other preparation
or configuration is needed.
SETTING UP
YOUR DOCUMENT FOR
DEBUG( )
As you build your document and its scripts, you
need to decide how granular you'd like debugging to be: global or function-by-function.
This decision affects the level at which you place the Boolean "switch"
that turns debugging on and off.
You can place one such switch as the first statement
in the first script of the page. For example, specify a clearly named variable
and assign either false or 0 to it so that its initial
setting is "off":
var DEBUG = 0
To turn debugging on at a later time, simply change
the value assigned to DEBUG from 0 to 1:
var DEBUG = 1
Be sure to reload the page each time you edit this
global value. Also, while you can use any variable name you like, avoid
the reserved keyword debugger (all lowercase). I chose all uppercase
DEBUG to help me locate and delete relevant statements once the page
is ready to deploy.
Alternatively, you can define a local DEBUG
variable in each function for which you intend to employ debugging.
One advantage of using function-specific debugging is that the list of
items to appear in the Java Console window will be limited to those of
immediate interest to you, rather than all debugging calls throughout the
document. You can turn each function's debugging facility on and off by
editing the values assigned to the local DEBUG variables.
CALLING DEBUG(
)
All that's left now is to insert the one-line
calls to debug() according to the following syntax:
debug(DEBUG,"label",value)
By passing the current value of DEBUG as
a parameter, you let the library function handle the decision to accumulate
and print the trace. The impact on your running code is kept to a one-line
statement that is easy to remember. To demonstrate, Example 2 consists
of a pair of related functions that convert a time in milliseconds to the
string format "hh:mm". To help me verify that values are being massaged
correctly, I insert a few calls to debug().
Example 2
function timeToString(input) {
var DEBUG = 1
debug(DEBUG,"input",input)
var rawTime = new Date(eval(input))
debug(DEBUG,"rawTime",rawTime)
var hrs = twoDigitString(rawTime.getHours())
var mins = twoDigitString(rawTime.getMinutes())
debug(DEBUG,"result", hrs + ":" + mins)
return hrs + ":" + mins
}
function twoDigitString(val) {
var DEBUG = 1
debug(DEBUG,"val",val)
return (val < 10) ? "0" + val : "" + val
}
After running the script, I display the Java Console
window in Navigator to see the following trace:
In timeToString(input): input=869854500000
In timeToString(input): rawTime=Fri Jul 25 11:15:00 Pacific Daylight Time 1997 (60 msec)
In twoDigitString(val): val=11 (0 msec)
In twoDigitString(val): val=15 (0 msec)
In timeToString(input): result=11:15 (220 msec)
Now you try it -- run
the script from Example 2. Show the Java Console window by choosing
Java Console in the Options menu (Navigator 3) or Communicator menu (Communicator).
Having the name of the function in the trace is
helpful in cases in which you might justifiably reuse variable names (for
example, i loop counters). The function names also help you see
more clearly when one function in your script calls another.
ABOUT THE TIMER
In the trace results of Example 2, you might wonder
how some statements appear to execute literally "in no time." You have
to take the timing values of this debug scheme with a grain of salt. For
one thing, these are not intended to be critical benchmarks. Some of the
processing in the debug() function itself occupies CPU cycles.
And with rapid-fire execution in the small scripts of Example 2, the timings
are not very meaningful. But in a more complex script -- especially one
involving numerous calls to subroutines and nested loops -- you can place
the debug() function calls in statements that aren't deeply nested,
so that the intervals won't be too small to be of value. In any case, regard
the values as relative, rather than absolute, values. And always run the
script several times to help you see a pattern of performance.
VIEWING IN REAL
TIME
I faced another debugging challenge recently while
using a computer with a small monitor that prevented me from viewing both
the Java Console and browser window at the same time. I wasn't convinced
that a nested for loop construction was iterating enough times
through a pair of related lengthy arrays. To help me visualize how the
loops were working as they ran, I modified the debug() function
to write the results not to the Java Console, but to the status bar at
the bottom of the browser window.
To do this, I replaced the line in debug()
that calls the System.out.println() function with a statement
that sets the window.status property:
window.status = msg
For this monitoring, the timing wasn't crucial, so
I commented out the lines in debug() that added the elapsed time
to the message. I placed the call to debug() in the innermost
loop, showing both of the loop-counting variables:
debug(DEBUG,"i,j", i + "," + j)
When I ran the script, the status bar whizzed through
the values of the for loop counters i and j:
i,j=0,0
i,j=0,1
i,j=0,2
...
i,j=1,0
i,j=1,1
(etc.)
In the status bar, I could examine the rhythm
of updating the values to see where some parts of the loop were slower
than others based on the array data being manipulated. (Notice, incidentally,
how the debug() function evaluated the third parameter to let
me join multiple values in a single debugging message.) And when the script
stopped, the status bar showed the final values, so I could see how far
the loops got.
SERVER-SIDE
APPLICATION
After seeing how helpful this debugging system
was on the client side, I looked to see if it could be useful on the server
side. Although the Netscape Enterprise Server application manager provides
a convenient trace function that's very good at detecting and revealing
syntax errors, it does not provide a way to view intermediate values of
expressions in your scripts. But you can modify the debug.js file
to operate as a server-side library file (compiled into the application).
Instead of writing trace results to the Java Console,
use the server-side write() method to output the accumulated trace
message to the current document. You can use the same DEBUG flags
either globally or within each function (the latter greatly recommended
in a complex server application). Of course, the finely crafted display
of the HTML output for the screen will be littered with your debug messages
whenever you have the DEBUG switches turned on. But that's OK,
because you should clear up all bugs and turn off (or remove) the DEBUG
flags before deploying the application.
STOMP 'EM YOUR
WAY
By standardizing a way of generating debugging
reports, you can greatly simplify the task of debugging your code. For
every HTML document you have under construction, include debug.js as
a library file. As you write each new function definition, automatically
include a debugging switch (var DEBUG = 0) as the first statement.
Then, when you're ready to capture values in trial runs of your script,
insert the three-parameter calls to debug() wherever they'll reveal
intermediate and resulting values to show you what's going on inside your
script while you blink. Over time you may also wish to modify the way the
output is formatted or the kinds of parameters you send to the debug()
function. Be aggressive, and don't let creepy crawly critters get
in your way.
View Source wants your feedback!
Write
to us and let us know
what you think of this article.
Author
and consultant Danny Goodman's
twenty-fifth book is JavaScript
Bible, published by IDG Books. His latest title, The Official
Marimba Guide to Bongo, encompasses the Castanet technology that is
built into Netscape Netcaster.
(8.97)
- Related Reading
Any sample code included above is provided for your use on an "AS IS" basis, under the Netscape License Agreement - Terms of Use