I wasted several hours debugging a problem last night in some JavaScript code, and now that (I think) I've got it sorted out, it's time to share the pain. The issue stemmed from my misunderstanding of how variable scoping works in JavaScript. The MDN docs on the var statement helped clear things up, but here's a few examples to demonstrate the problem.

Example 1

Let's declare a variable i that's set to 5, then call a function that defines another i as part of a for loop. Unsurprisingly, the inner loop prints out 1 through 9, while the first and last statements prints 5 (the value of the original i variable).

function counter() {
	for (var i = 0; i < 10; i++) {
		document.writeln(i);    
  }
}

var i = 5;
document.writeln(i + '<br>');    // 5

counter();                       // 0-9 (counter sets i to 0)

document.writeln('<br>' + i);    // 5

// output:
// 5
// 0 1 2 3 4 5 6 7 8 9 
// 5

No surprises so far. That did exactly what I'd expect it to do. But what if we leave the var keyword out of the declaration of i in the loop? My assumption was that it'd behave the same way... oops.

function counter() {
	for (i = 0; i < 10; i++) {
		document.writeln(i);    
  }
}

var i = 5;
document.writeln(i + '<br>');    // 5

counter();                       // 0-9 (counter sets i to 0)

document.writeln('<br>' + i);    // 5 ?

// output:
// 5
// 0 1 2 3 4 5 6 7 8 9 
// 10

So what happened the second time around? The i inside of counter() was not redeclared (no var this time), so it re-used the same i declared outside of the function, which is a global variable.

The scope of a variable declared with var is its current execution context, which is either the enclosing function (our first example) or, for variables declared outside any function, global (our second example).

Example 2

Here's another example, where i is declared outside of a loop, and then again inside the loop. So, after reading the above, I'd consider these 2 separate i variables to be within different contexts. That'd be consistent with other languages.

var i = 5;
for (var i = 0; i <= 9; i++) {
  document.writeln(i);
}
document.writeln('<br>' + i + '<br>');

// output:
// 0 1 2 3 4 5 6 7 8 9 
// 10

Wrong again, apparently.

True to the docs, the different context seems to only occur at the function level. If we move the for loop inside a function and call that, then the two i variables are indeed treated differently. Leave out the var keyword on the loop, however, and we'll end up with the same result as the second example in the last section.

var i = 5;
  
function loopFunc() {
  for (var i = 0; i <= 9; i++) {
    document.writeln(i);
  }
}

loopFunc();
document.writeln('<br>' + i + '<br>');

// output:
// 0 1 2 3 4 5 6 7 8 9 
// 5

It takes a village...

Update: After writing this up and lamenting on Twitter, I got two great responses with two great solutions.

Let

First, Dave Johnson told me about the let keyword, which creates a new binding for the variable on every iteration, and separate from any other variable of the same name outside the loop.

So if you retry the first example in the previous section, substituting let for var, the answer is more inline with what you might expect in other languages.

var i = 5;
for (let i = 0; i <= 9; i++) {
  document.writeln(i);
}
document.writeln('<br>' + i + '<br>');

// output:
// 0 1 2 3 4 5 6 7 8 9 
// 5

Read more: Exploring ES6 - let and const in loop heads

Use Strict

A little while later, Axel Rauschmayer (author of the above article, which is part of a book) told me about strict mode, which causes an error to be thrown if a variable is not declared. It solves a slightly different problem.

I mentioned that we can completely forego declaring i and it'll simply be created as a global variable for us. However, if we specify "use strict" then it'll throw an error instead!

"use strict";

try {
  for (i = 0; i <= 9; i++) {
    document.writeln(i);
  }
	document.writeln('<br>' + i + '<br>');
}
catch (err) {
  document.writeln(err);
}

// output:
// ReferenceError: i is not defined

What'd I Learn?

Um, always read the docs? Heh.

JavaScript is quite a dynamic language, and full of surprises. I'd expect one of two things instead of what actually occurred:

  • The var keyword is optional, and omitting it still creates a separate i variable in its own scope.
  • The var keyword is required, and omitting it throws a syntax error.

What we actually get is that by omitting the var keyword, it simply looks outside of the function for any identically named variable that was declared before the function was called, and uses that instead. We don't even have to mark the outer variable as global - it simply is.

My actual problem was more complex, involving recursion... actually, I haven't exactly figured out why the problem was occurring where it was, but the solution was the same.

I had a function that used i inside a loop without ever declaring it with var, and the loop called the same function again. Even though i was declared nowhere else in my script, somehow it led to the function being called recursively forever. Once I introduced the var keyword, everything worked as expected and the function stopped being called recursively as soon as the condition that should stop it was true.

JavaScript. sigh...