Total Pageviews

Monday, September 2, 2013

The Young Developer\'s Guide to Debugging JavaScript

We recently completed our internship program here at The Times, and it's made me wonder what I would have liked to have known when I was a young developer. The answer: I wish I'd known more about debugging.

There is no shortage of resources on how to use the various browser dev tools, and new tools are added daily. They are amazing. As someone who learned JavaScript years ago, I envy new developers for these tools. To set a breakpoint in a browser, inspect all values in the environment and walk up the call stack has been transformative.

I would have loved to have had such magical toys while learning: breakpoints, CoffeeScript, source maps, network inspector, reliable ubiquitous console.

That said, your strongest debugging tool is the one between your ears. All the arcane debugging knowledge in the world is no substitute for understanding what you're coding.

To Develop Is to Err

To write code is to make mistakes. The best developers you've ever met have been responsible for bugs. They have sat at their computers, scratched their heads and wondered, “Well why is it doing that?”

Developers often think of programming as problem solving, but writing code is more like cooking. As with cooking, code is never perfect, only better and worse. You can try different spices. You can use turkey instead of chicken. You can apply more or less heat. But there is no complete, only done enough. Dinner is done when you eat it.

To master debugging, you must expect to find bugs. If somebody reports a bug, you should accept it. The natural state of code is to have bugs.

Where Bugs Come From

If you are new to writing code, the most likely cause of a bug is your nascent understanding of the platform. If you don't have a precise knowledge of how an array works, you are likely to misuse arrays, and that will cause bugs. Only experience can correct this.

This guide assumes you have reached a point of sufficient expertise that your bug is not because you don't understand how a given feature works. Don't worry: You will still create bugs.

You're likely to encounter two kinds of tricky bugs:

  1. Subtle Typos
  2. Wrong Object Types

Whenever you say the phrase “I don't understand why x,” stop yourself and try to remember that if x is doing something, it is because that thing makes absolute sense to x. X's behavior just seems puzzling to you.

From the perspective of the computer and the program, all behaviors are as expected, given the rotten input provided. If you don't understand why your program is doing what it is doing, the problem is your lack of understanding.

The computer is always right. The computer is always right. The computer is always right. Take it from someone who has programmed for over ten years: not once has the computational mechanism of the machine malfunctioned.

Subtle Typos

Most typos are spectacular, resulting in names that don't exist, which throws an undefined variable error. This will spew red pixels all over your toolset and is usually easy to catch. The pernicious bug results from typing a name or value that is defined, but is not what you intended. You can add a linter to your toolset, but you can't rely on that to catch everything.

For instance, you might have a hash of values. In JavaScript, a variable inside a hash that has no value returns null. If the value is an object with methods, calling a nonexistent method should error out meaningfully. Helpful red pixels everywhere. But if you are merely accessing a value which happens to be null, you may get incorrect math values that aren't as easy to track. My console informs me that null - 3 == -3.

Wrong Object Type

In this age of Gmail and Facebook, client applications can be thousands and thousands of lines of code, with complex object hierarchies. Template engines render data models delivered by transport objects controlled by framework controllers controlling view controllers.

Modern applications have many object types, and if you happen to use the wrong type - particularly if the types are similar, or perhaps parent or child classes of the one you intended - most things will work correctly, but some will not. It is important to check that the object producing bugs is always the type you expect and that all variables you use in that object are the type you expect.

As an example, suppose you have an important set of data in a hash. Note: The examples that follow are in CoffeeScript, but are illustrative. Think of them as pseudocode that happens to execute.

Animals =       “Fido”: DogObject(“Fido”)      “Samantha”: CatObject(“Samantha”)      ...  

Sometimes your code will expect the key and sometimes the actual object. A common trap is to expect one and get the other.

# Is this the object or the string Fido?  addAgeToAnimal: (animal, age) -> animal.setAge(age)   

Or suppose DogObject extends AnimalObject, and you are pulling from a database. You create AnimalObjects and automatically fill them in with data. When calling the method, sometimes the method will get an actual DogObject, and sometimes it will get an AnimalObject that you've filled with dog data. But then you change DogObject, and your manual AnimalObjects are missing a now-required piece of information. This can be tricky to figure out. (Consider an alternate approach, by the way. Try to minimize divergent code paths.)

# AnimalObject doesn't have a buyBone method.  buyDogNewBone: (dog) -> dog.buyBone()  
Next Steps: Problem Areas

Once you determine what the problem area is, you must determine why the value is incorrect.

1. Async

Your brain probably does a poor job understanding asynchronous operations. It's hard enough tracking vast application state trees even when you're not adding time as a variable. Yet asynchronous operations are the rule in JavaScript applications. If an object is the wrong type or the wrong value, the odds are good that you have a race condition caused by an asynchronous operation (via an AJAX call, an asynchronous database or worker call). Or you could just be using Node.

# What does a equal? Depends when you ask.    @a = “Default”    jQuery.getJSON destinationUrl, (data) =>      @a = data.people[0].firstName    @a = “Bob”  

When a bug surfaces, you will need to use breakpoints in your debugger to pause execution and inspect application state at various times. If the problem does not emerge, you'll probably need to check many iterations of a piece of code through time. The place to start is with any asynchronously delivered data.

2. Counting Problems and Off By One

This is so common it's a programmer joke. When looping through an object, make sure the object is the type you expect, contains all the properties you expect and is consistent. If components are sharing state, as in the async scenario above, a loop could be compromised between loop runs. If you cached a count value and used it to run a loop without checking that it's still valid, loop errors become more likely. If you are counting in one-based systems and zero-based arrays simultaneously, the odds of a bug rise even more quickly.

jQuery.getJSON remoteUrl, (data) =>      ###      # Data is of form: [      #    {      #       “id”: 1,      #       “first”: “Bob”,      #       “last”: “Smith”      # }...      #]      ###      names = []      data.forEach (item) => names[item.id] = item.first + ‘ ‘ + item.last  ###  # Uh oh. Depended on the id being zero based, but it's one   # based, and arrays aren't.  # for (var i=0, len=... will render unexpected results.  ###  
3. Scope

Scope is a problem in many environments, but the problem is particularly nasty in JavaScript. One of the easiest ways for a variable to have an unexpected value is for the scope to be different than expected. If you define a local variable without using var, the value leaks up to enclosing scopes. The JavaScript keyword this doesn't always mean what you expect it to mean. When pausing on a breakpoint, make sure this is the object you expected. The easiest mistake is in an event handler or a setTimeout. By default, this in either scenario will be the global window object. (My solution is to use CoffeeScript and the fat arrow. There are other solutions. And ES6, the next version of JavaScript, will also have solutions.)

# In setTimeout, this will translate to window.removeFlag  unflag = -> @removeFlag()   setTimeout(unflag, 500)  
Talk to Somebody

Finally, and perhaps more importantly, you should talk to somebody about your bugs. I've heard it called many things, but for me, it will always be “duck debugging.” The premise is to put a rubber duck next to your computer, and whenever you encounter a bug, explain it to the duck. Better yet, talk to another developer.

You'll often find that by verbally expressing the problem, your brain will solve it before the person listening even needs to speak.

Bugs are a fundamental aspect of programming. You will continue to make them for the rest of your career. Fixing them is the most important thing we do as developers. The quicker you can resolve bugs, the quicker you can return to features and fancy code designs.

Bugs are a signal that your understanding of a problem is incomplete or mistaken. Reality is the final arbiter. There will be moments, after staring at your screen all day. You want to throw your computer out the window. And then, somehow, through effort, duck debugging, or maybe just dumb luck (I strongly advise taking the occasional walk), you break through. This is the reward. Knowledge, hard won, will burst in your mind, and you will understand the world around you that much better. I can't emphasize how wonderful that feeling is. You are one step closer to unattainable perfection.

Congratulations. You are now a debugger.



No comments:

Post a Comment