Tips For Debugging JavaScript Race Conditions

Race conditions are tedious to debug, even in a single-threaded language like JavaScript. Here are three tips I hope you never need.

1. Breaking On DOM Changes

When dealing with nested views and asynchronous renders, it’s common to run into issues where a subview fails to appear reliably.

To debug these issues, I like using Chrome’s ability to break on DOM changes. Right click on a node in the Elements panel of your dev tools and you’ll be presented with different types of breakpoints you can set.


2. Spying On Object Properties

A particularly frustrating problem that results from improperly cloning objects is when properties change their values seemingly at random.

// One source of this problem is devious .toJSON methods
// that don't clone their source object
var myModel = new Backbone.Model({uhoh: {nested: 'somevalue'})}  
var copy = myModel.toJSON()  
copy.uhoh.nested = 'changed'  
myModel.get('uhoh').nested == 'changed'  

View on jsfiddle

Here the source of the mischief is clear, but you won’t always be so lucky. If the code that manipulates the object is in some asynchronous callback three modules deep, its not trivial to figure out where the source of the problem is since there are so many ways to manipulate the value:

// The many different ways to cause this problem
myModel.get('uhoh').nested = 'changed'  
myModel.toJSON().uhoh.nested = 'changed'  
myModel.attributes.uhoh.nested = 'changed'  

You can handle all these cases by defining a setter on the uhoh object with a debugger breakpoint in it:

myModel.attributes.uhoh.__defineSetter__('nested', function(val) {  
  uhoh._nested = val

View on jsfiddle

Now you’ll be able to look at the call stack no matter how that property was changed.

3. Making It Worse

Debugging is hard because it’s the inverse of programming: “Given the code, pinpoint the source of this behavior”. This is especially true when dealing with race conditions, because the behavior doesn’t happen all the time. This is why when all else fails, I solve the inverse problem: writing code that makes the bug happen reliably.

If you can make the issue 100% reproducible, not only have you gained insight into the root cause of the problem, you’ve also shortened the debugging cycle.

In general, this is what I do:

  1. Locate the sections of code that manipulate the misbehaving data
  2. Wrap those sections in .setTimeout with large-ish intervals to force an deterministic order of operations
  3. Permute the order of operations until the problem is 100% reproducible
  4. Permute the order of operations until the problem is gone

I like to use prime numbers as intervals to reduce the odds that multiple sections of code run in close temporal proximity to each other because they happened to be repeating in phase.

I hope these tips save you as much time as they’ve saved me.dom-breakpoint-screenshot.png