A List Apart

Menu

Advanced Debugging With JavaScript

by , Published in JavaScript, Workflow & Tools · 22 Comments

When used effectively, JavaScript debuggers help find and squash errors in your JavaScript code. To become an advanced JavaScript debugger, you’ll need to know about the debuggers available to you, the typical JavaScript debugging workflow, and code requirements for effective debugging. In this article, we’ll discuss advanced debugging techniques for diagnosing and treating bugs using a sample web application.

On accessibility

Article Continues Below

This article highlights strengths and differences between debugging tools, and shows how we perform advanced JavaScript debugging tasks. Our methods often rely on the mouse; if you prefer to use keyboard shortcuts, or if you rely on assistive technologies such as screen readers to interact with these tools, you should consult the relevant documentation to determine how (or if) these tools will work for you.

The debuggers

With an increasing selection of good debuggers available, JavaScript programmers can gain a lot from learning how to use them. JavaScript debugger user interfaces (UIs) are becoming more polished, more standardized across products, and easier to use, thus making it easier for both experts and novices to learn JavaScript debugging.

Currently, there are debugging tools available for all major web browsers.

Currently, Firebug and Dragonfly are the most stable options. IE8’s beta tools sometimes ignore breakpoints, and at the time this article was written, WebInspector seemed to have compatibility issues with the nightly builds of Webkit.

Familiarize yourself with multiple debugging tools—you never know in which browser the next bug will arise.  Since the debuggers are roughly comparable in functionality, it’s easy to switch between them once you know how to use one.

Debugging workflow

When investigating a specific problem, you will usually follow this process:

  1. Find the relevant code in the debugger’s code view pane.
  2. Set breakpoint(s) where you think interesting things may occur.
  3. Run the script again by reloading the page in the browser if it’s an inline script, or by clicking a button if it’s an event handler.
  4. Wait until the debugger pauses execution and makes it possible to step through the code.
  5. Investigate the values of variables. For example, look for variables that are undefined when they should contain a value, or return “false” when you expect them to return “true.”
  6. If necessary, use the command line to evaluate code or change variables for testing.
  7. Find the problem by learning which piece of code or input caused the error conditions.

To create a breakpoint, you can also add the statement debugger to your code:

function frmSubmit(event){
    event = event || window.event;
    debugger;
    var form = this;
}

Debugger requirements

Most debuggers require well-formatted code. Scripts written on one line make it difficult to spot errors in line-by-line debuggers. Obfuscated code can be hard to debug, especially code that has been “packed” and needs to be unpacked using eval(). Many JavaScript libraries allow you to choose between packed/obfuscated and well-formatted versions, making it easy to use formatted code for debugging.

Debugging demo

Let’s start with a small, bug-ridden example, to learn how to diagnose and treat each ailment in turn. Our example is a web application login screen.

Imagine you’re working on this fabulous new web application, and your testers ask you to investigate this list of bugs:

  1. The “Loading…” status bar message does not disappear when the application finishes loading.
  2. The language defaults to Norwegian even in English versions of Firefox and IE.
  3. A global variable named prop is created somewhere.
  4. In the DOM viewer, all elements have a “clone” attribute.
  5. “Minimum length” form validation doesn’t work—trying to submit the form with a one-letter user name should cause an error message.
  6. Leaving the password field blank is allowed—you should see an error message saying that the password field may not be empty, on submitting the form.
  7. The login always fails, with an error message saying that a cross-site request forgery attack was detected.

Launching debuggers

  • In Firefox you need to make sure you have the Firebug extension installed. Select “Tools > Firebug > Open Firebug” to get started.
  • In Opera 9.5 beta 2 or later, choose “Tools > Advanced > Developer Tools.”
  • In IE8 beta, choose “Tools > Developer Tools.”
  • In Safari or WebKit, first enable the debug menu, then choose “Debug > Show Web Inspector.”

It’s time to fire up the debuggers. To follow the demo, pay close attention to the step-by-step instructions throughout the remainder of the article. Since some instructions involve modifying the code, you may want to save the page locally and load it from your file system before starting.

Bug one: the “Loading…” message in the status bar

If you look at the debugging applications in Dragonfly and Firebug, you’ll get an initial view as seen in figure one.

Dragonfly debugger with the first inline script in page selected
Firebug debugger with script tab open for source inspection

fig. 1: the initial view of our application’s JavaScript in Dragonfly and Firebug, respectively.

When you look at the source code in the debugger, note that there is a function clearLoadingMessage() defined at the top of the code. This seems like a good place to set a breakpoint.

Here’s how to do it:

  1. Click the line number in the left margin to set a breakpoint on the first line inside the function clearLoadingMessage().
  2. Reload the page.

Note: the breakpoint must be set on a line with code that will execute when the function runs. The line that contains function clearLoadingMessage(){ is just the function signature. Setting a breakpoint there will never actually cause the debugger to stop. Set your breakpoint on the first line inside the function instead.

When the page is reloaded, script execution stops at the breakpoint and you’ll see an output like the one shown in figure two. (Dragonfly is shown at the top, Firebug at the bottom.)

Dragonfly debugger stopped at breakpoint inside clearLoadingMessage
Firebug debugger stopped at breakpoint inside clearLoadingMessage

fig. 2: the debuggers stopped at a breakpoint inside clearLoadingMessage.

Let’s step through the function. You’ll see that it updates two DOM elements, and on line 28 it mentions the word statusbar. That looks significant. It’s likely that getElements( ‘p’, {'class':'statusbar'} )[0] will find the statusbar element in the DOM. Is there a way you can test that theory quickly?

Paste the relevant snippet into the command line to check your theory. Figure three shows three screenshots (Dragonfly, Firebug, and IE8) after reading the innerHTML or outerHTML of the element returned by the command you’re investigating.

To test this assumption:

  1. Find the command line:
           
    • In Firebug, switch to the “Console” tab.
    •      
    • In Dragonfly, look below the JavaScript source code pane.
    •      
    • In IE8 Developer Tools, find the tab on the right labeled “Console.”
    •   
  2. Paste getElements( ‘p’, {'class':'statusbar'} )[0][removed] into the command line.
  3. Press enter.
Dragonfly debugger command line with innerHTML output
Firebug debugger command line with innerHTML output
IE8 debugger command line with outerHTML output

fig. 3: output shown in Dragonfly, Firebug, and IE8, respectively.

The command line is a very useful tool that allows you to test small script snippets quickly. Firebug’s console integration is very useful—if your command outputs an object, you get an intelligent view. For example, you get a markup-like representation if it is a DOM object.

You can use the command line to explore the problem in more depth. The line of JavaScript we’re looking at does the following three things:

  1. It gets a reference to the status bar element. In the DOM inspector view, you will see that the corresponding markup is <p class=“statusbar”>.
  2. It looks up its firstChild, in other words, the first node inside this paragraph element.
  3. It sets the innerText property.

Let’s try to run a bit more of that command from the command line. (Tip: use the up arrow key to navigate back to previous lines typed into the command line field.) For example, you might wonder what the current value of this element’s innerText property is before it is set. To check this, you can type the entire command up to the equals sign into the command line:

getElements( ‘p’, {'class':'statusbar'} )[0].firstChild.innerText

Surprisingly, the output is…nothing. So the expression getElements( ‘p’, {'class':'statusbar'} )[0].firstChild points to something in the DOM that does not contain any text, or does not have an innerText property.

So, the next question is: what exactly is the first child node of the paragraph element? Let’s ask the command line that question. (See figure four for the result.)

Dragonfly debugger command line outputs [object Text]

fig. 4: the Dragonfly debugger command line, outputting [object Text].

Dragonfly’s [object Text] output shows that this is a DOM text node. Firebug shows us the content of the text node as a link to the DOM explorer. You have now found the problem that causes bug number one: a text node does not have an innerText property—only element nodes do. Hence, setting p.firstChild.innerText does nothing. This bug can be fixed by replacing innerText with nodeValue, which is a property the W3C DOM standard defines on text nodes.

After you’ve had a chance to review this example:

  1. Press [F5] or the run button to finish the script, now that you’ve found the problem.
  2. Remember to clear the old breakpoint by clicking the line number again.

Bug two: problematic language detection

You may have noticed the var lang; /*language*/ line near the top of the JavaScript—the code that sets this variable is probably involved when things go wrong. You can find things in the code quickly using the handy search functions provided by both debuggers. In Dragonfly it’s just above the code viewer; in Firebug it’s on the top right side of the Firebug user interface. (See figure five.)

To find the place that deals with the application’s localization:

  1. Type lang = into the search field.
  2. Set a breakpoint on the line where the lang variable is assigned a value.
  3. Reload the page.

Safari’s WebInspector also has a very powerful search feature. WebInspector allows you to search all the code at the same time, including markup, CSS, and JavaScript. The results are shown in a dedicated pane where you can doubleclick them to jump to the source line, as shown in the screenshot.

Dragonfly debugger search field
WebInspector debugger search field

fig. 5: searching with the Dragonfly and WebInspector debuggers.

To inspect what this function does:

  1. Use the “step into” button to enter the getLanguage function.
  2. Click the “step into” button repeatedly to go through the code one line at a time.
  3. Keep an eye on the overview of local variables to see how they change while you step through the function.

Stepping into the getLanguage function, you will see that it tries reading the language from the user-agent string. The author of this code noticed that some language information is included in the user-agent string on some browsers, and tries to parse navigator.userAgent to extract this information:

var str1 = navigator.userAgent.match( /((.*))/ )[1];
var ar1 = str1.split(/s*;s*/), lang;
for (var i = 0; i < ar1.length; i++){
    if (ar1<i>.match(/^(.{2})$/)){
        lang = ar1<i>;
    }
}

While stepping through this code with the debuggers, you can use the local variables overview. Figure six shows Firebug and IE8 Developer Tools with the ar1 array expanded to show the elements in it:

Firebug and IE8's local variables pane while running getLanguage function

fig. 6: Firebug and IE8’s local variable panes, while running the getLanguage function.

The ar1<i>.match(/^(.{2})$/) statements simply look for a string that is exactly two characters long, expecting a two-character language code such as en or no. However, as you can see from the screenshots, Firefox’s language information in the user-agent string is actually nn-NO. IE has no language-related information in this part of the userAgent string.

The second bug has been found: the language detection looks for a two-letter language code, but Firefox has a five letter locale string and IE has nothing. This “language detection” code should probably be scrapped altogether, and replaced with something server-side that uses the HTTP Accept-Language header or possibly falls back to reading navigator.language. Or navigator.userLanguage in IE. One example of what that function might look like is shown below:

function getLanguage() {
    var lang;    if (navigator.language) {
        lang = navigator.language;
    } else if (navigator.userLanguage) {
        lang = navigator.userLanguage;
    }    if (lang && lang.length > 2) {
        lang = lang.substring(0, 2);
    }    return lang;
}

Bug three: the mystery prop variable

Firebug's local variables pane shows the global prop variable
Dragonfly's local variables pane shows the global prop variable

fig. 7: Firebug and Dragonfly’s local variable panes showing the global prop variable.

In figure seven, you can clearly see the mystery prop variable. Well-written applications keep the number of global variables to a minimum, because they can cause confusion when different sections of the application try to use the same variable name. Imagine that tomorrow,  another team of developers added a new feature to our application and also named a variable prop. We’d have two different parts of our application trying to use the same variable name for different things. This practice is a recipe for conflicts and bugs down the road. Hence, you have to track down where this variable is set and see if there is a way to make it a local variable. You could start searching like we did to find bug two above, but perhaps there is a smarter way…

Debuggers for many other programming languages have a “watch” concept that can break into the debugger when a variable changes. Neither Dragonfly nor Firebug currently support “watch” but it’s easy to get the same effect by adding the following line of debugging code at the top of the source of the script you’re troubleshooting:

__defineSetter__(‘prop’, function() { debugger; });

To add this “watch”-like functionality to the debugged script:

  1. Add the debugging code to the top of the first script.
  2. Reload the page.
  3. Note how it breaks when the problem occurs.

Using getters and setters can emulate “watch” functionality and help you set “smart” breakpoints.

The IE8 Developer Tools have a “watch” pane, but it doesn’t break when a watched variable is modified. Given IE8’s incomplete support for getters and setters, you can’t emulate this functionality the way you can in Firefox, Opera, and Safari.

When you reload the application, it will immediately break where the global variable prop is being defined. It actually stops at the line of debugging code you added because this is where the debugger statement is. One click on the “step out” button will take you from the setter function to the place where the variable is set. This code is found inside the getElements function: (Line wraps marked » —Ed.)

for (prop in attributes) {
    if (el.getAttribute(prop) != attributes[prop]) includeThisElement»
= false;

It now stops just below the line that starts with for (prop. Here, you can see that the prop variable is used without being defined as a local variable, with a var keyword inside the function. Simply changing it to for (var prop will fix our third bug.

Bug four: the clone attribute that shouldn’t be there

The fourth bug was obviously found by a thorough tester using a DOM inspector, because it’s not visible from the user interface of the application. But, as soon as you open the DOM inspectors (in Firebug it’s the “HTML” tab, while in Dragonfly the feature is called “DOM”) it becomes clear that many elements have clone attributes with JavaScript source code in them that shouldn’t be there:

Dragonfly's DOM inspector shows the problematic code

fig. 8: Dragonfly’s DOM inspector shows the problematic code.

Every element created by the script has a superfluous clone attribute. Since it’s something the end user will never notice from the user interface, it may not appear to be a serious bug. But imagine the performance impact if this script is used to create a DOM tree with hundreds or thousands of elements…

The fastest way to find this problem is to set a breakpoint that only occurs when an attribute named clone is set on any HTML element. Can our debuggers handle that?

JavaScript is a very flexible language, and one of its strengths (or quirks, depending on your point of view) is that you can replace core functions with your own. Add this snippet of debugging code to the page, and it will replace the original setAttribute() method with one that breaks if a clone attribute is set:

var funcSetAttr = Element.prototype.setAttribute; /* keep a pointer
 to the original function */Element.prototype.setAttribute = function(name, value) {
    if (name == 'clone') {
        debugger; /* break if script sets the 'clone' attribute */
    }
    funcSetAttr.call(this,name,value);  /* call original function to
    ensure those other attributes are set correctly */
};

To find the clone attribute creation point:

  1. Add the debugging code at the top of the first script in the document.
  2. Reload the page.

As soon as you reload the page, the script starts generating the DOM, but breaks the first time it attempts to set the bad attribute. (Note that in current versions of Firefox, setAttribute has various element-specific implementations. The code above only works as expected in Opera; to get the same effect in Firefox you can replace the word Element with HTMLFormElement to replace the more specific HTMLFormElement.prototype.setAttribute method.)

When execution stops at the breakpoint, you’ll want to know where the setAttribute() call occurred. This means you have to go back up the list of called functions and figure out how the script got here. For this, you’ll use the call stack.

Figure nine shows Dragonfly’s and IE8’s call stacks when stopped at the breakpoint. Just for reference, I’ve created a manual breakpoint in IE8 at roughly the same place the setAttribute() technique stops. You can see that in Dragonfly the most recently called function is at the top—it’s the anonymous function that was mapped to setAttribute. It was called from makeElement at line 95:

Dragonfly and IE8's Developer Tools' call stack

fig. 9: Dragonfly and IE8’s call stacks.

Figure 10 shows Firebug’s call stack view. The setAttribute < makeElement < init line next to the file name is the stack—and you can see that the most recently called function is the leftmost one:

Firebug’s call stack view

fig. 10: The version of Firebug we used shows the callstack on one line. The last function called is on the left.

By clicking earlier functions in the stack list you can step back through the calls and look at how you arrived here. It’s important to try this yourself to fully understand how powerful this feature is. Note that when you jump to another stack entry, the local variable pane is updated to show you the current state of the variables in the calling function.

How to use the call stack to find the offending function:

  1. Click the function you want to see in the call stack.
  2. Note that the local variables pane updates to show the variables local to that function.
  3. Remember that if you use the step buttons, they will take you onwards from the last call even if you are inspecting other parts of the stack.

Clicking the makeElement reference takes us back to this part:

for (var prop in attributes) {
    el.setAttribute(prop, attributes[prop]);
}

where you see the setAttribute call. The local variables/frame inspection panes show that the value of prop is indeed clone. The variable prop is defined in the for…in loop. This tells us that one of the properties of the attributes object must be named clone.

The attributes variable is defined as the second named argument of that function. If you go one step up to the init part of the stack, you can see that the previous function call was this:

var form = makeElement(‘form’, { action:’/login’, method:’post’, name:’loginform’ }, document.body);

The second argument to the method is highlighted in bold—this object does not have a clone property. So where does it come from?

If you click the makeElement function in the stack again, you can inspect the attributes variable. In Firebug you’ll see the clone property appear. You can click the function it references to jump to the definition—note the line highlighted in blue in Figure 11:

Firebug shows us where the clone property was defined

fig. 11: Firebug shows us where the clone property was defined.

The Dragonfly alpha does not include properties of an object’s prototype in the variable inspection view, so clone does not appear there. But now you’ve learned that a clone function exists on all objects. If you enable the “stop at new script” setting, and create a new thread calling this method, you’ll jump right to it.

To find the clone definition in Dragonfly:

  1. Toggle the “stop at new script” option with the user interface button shown in this screenshot:
    Dragonfly’s stop at new script button
  2. Paste the following code into the address bar of the window you are debugging:
    [removed]void({}.clone());

With a bookmarklet, you can start new threads in the debugged document at will. For example, you could try [removed]void({}.clone()); to start a new thread that will call the clone() method. When Dragonfly stops at the new thread, one click on the “step into” button jumps to the problematic definition of the clone property. (Note that a Dragonfly bug may require disabling User JavaScript to get this working.)

And here’s the cause of the fourth bug: a clone method is added to all objects using Object.prototype syntax. This is considered a bad practice, because using for…in on objects will see everything you add to Object.prototype. This may create bugs that are very hard to track down.

To fix this bug, you could move the clone method so that it is defined directly on the Object object instead of on the prototype, and then fix any calls to obj.clone() so that they become Object.clone(obj) instead. For example, avoid this:

 // BAD, don't do this:
Object.prototype.clone = function() {
    var obj = {};
    for (var prop in this) {
        obj[prop] = this[prop];
    }
    return obj;
}// some code that demonstrates using the clone() method:
var myObj1 = { 'id': '1' };
var myObj2 = myObj1.clone();

Do this instead:

Object.clone = function(originalObject) {
    var obj = {};
    for (var prop in originalObject) {
        obj[prop] = originalObject[prop];
    }
    return obj;
}// some code that demonstrates using the clone() method:
var myObj1 = { 'id': '1' };
var myObj2 = Object.clone(myObj1);

The latter example avoids modifying Object.prototype and thus works better with other scripts using for…in type loops.

Bugs five through seven

Now, dear reader, you are on your own. Using your new skills, bugs five, six, and seven are up to you to fix! Good luck and have fun.

Summary

This article demonstrates the basics of using debuggers and some advanced JavaScript debugging techniques. You’ve learned how to set break points both from the debugger and from scripts, how to step through code, how to use the debugger user interface, how to set advanced break points, and how to integrate bookmarklets in your debugging techniques.

If you had problems following the more advanced parts of this article, don’t worry! Mastering the basics first will make you a much better developer and eventually, you will develop your own set of interesting techniques to share.

About the Authors

22 Reader Comments

Load Comments