Loops and asynchronous operations

Firing off asynchronous operations from inside a loop is one of the most often screwed up things in JavaScript. All of the same properties that so often come to our aid and make our lives easier combine to blind side us in a hard to spot way.

The crux of the problem is scope. A JavaScript function can access any variable that is defined where the function is defined, but they have their current value, not necessarily the same as when the function was defined.

var input = [ 'foo', 'bar', 'baz']; var orderedOutput = []; // or is it? for (var i = 0; i < input.length; i++) { // 'i' right here has the value you expect, // counting from 0 to 2. Same with the line // below where we get the thing from the input // array. doAsyncThing(input[i], function (err, results) { orderedOutput[i] = results; // 'i' here is 3. // why 3? because this is the future. // we are referencing the outside i which at // this point has gone through the // entire array, failed the check // to make sure it is shorter than // 'input.length' and is sitting there unused. console.log(orderedOutput); // Each time this function is called i will // always be 3, so ordered output will look // something like: "[ , , , 'We got: foo' ]" // each time it gets logged, with the message // changing each time. }) } function doAsyncThing (thing, callback) { // wait up to a second. setTimeout(function () { callback(null, 'We got: ' + thing) }, 1000 * Math.random()); }

The easy answer to the problem above is to use es5's [].forEach, or lodash's _.forEach to iterate over the array instead of a for loop. That way each iteration gets its own copy of i who's value never changes, because each one is defined in its own function scope.

var input = [ 'foo', 'bar', 'baz']; var orderedOutput = []; // or is it? input.forEach(function (thing, i) { doAsyncThing(thing, function (err, results) { orderedOutput[i] = results; // 'i' here is the i that corresponds // to the correct item. console.log(orderedOutput); }) }) function doAsyncThing (thing, callback) { // wait up to a second. setTimeout(function () { callback(null, 'We got: ' + thing) }, 1000 * Math.random()); }