3

The following example code confuses me...

"use strict";

var filesToLoad = [ 'fileA','fileB','fileC' ];
var promiseArray = [];

for( let i in filesToLoad ) {
  promiseArray.push(
    new Promise( function(resolve, reject ) {
      setTimeout( function() {
        resolve( filesToLoad[i] );
      }, Math.random() * 1000 );
    })
  );
}

Promise.all( promiseArray ).then(function(value) {
  console.log(value);
});

The reason I'm confused is that I was expecting a random ordered output on the console. But I always get the following...

[ 'fileA', 'fileB', 'fileC' ]

That confuses me little to say the least, but what really gets me scratching my head is when I change the let i to var i I get the following result....

[ 'fileC', 'fileC', 'fileC' ]

As someone who has only recently tried to fully understand Promises and not that long ago starting using let, I'm really stumped.

Further reading...

After getting lots of great answers I have refactored the example to get rid of the loop and i. Will seem obvious to most, but fun for me...

"use strict";

var filesToLoad = [ 'fileA','fileB','fileC' ];

function start( name )  {
    return new Promise( function(resolve, reject ) {
      setTimeout( function() {
        resolve( name + '_done' );
      }, Math.random() * 1000 );
    });
}

Promise.all( filesToLoad.map(start) ).then(function(value) {
  console.log(value);
});
thomas-peter
  • 7,738
  • 6
  • 43
  • 58

5 Answers5

4

It is because of closure. Read about it here and here.

Also let is block scoped whereas var is function scoped.

In case of using var i:

After timeout when the function is triggered, looping was completed and i was set to 2. so it got resolved with filesToLoad[2] for all the setTimeout functions.

In case of using let i:

Since it is block scoped, when function is resolved it remembers the state of i when setTimeOut was declared, so when it gets resolved it uses correct value of i.

Regarding the order of output in case of using let i.

Promise.all(Iterable<any>|Promise<Iterable<any>> input) -> Promise

Given an Iterable(arrays are Iterable), or a promise of an Iterable, which produces promises (or a mix of promises and values), iterate over all the values in the Iterable into an array and return a promise that is fulfilled when all the items in the array are fulfilled. The promise's fulfillment value is an array with fulfillment values at respective positions to the original array. If any promise in the array rejects, the returned promise is rejected with the rejection reason.

So irrespective of order in which your promises gets resolved, the result of promise.all will always have promise resolve value in correct order.

Community
  • 1
  • 1
Kunal Kapadia
  • 3,223
  • 2
  • 28
  • 36
2

Why using let and var produces different results:

The reason that using let produces the desired result over var is that when using let you declare a block-scoped variable, such that when the loop moves to another iteration the value of i remains unaffected for the contents of the loop at that time.

Defining a var variable in the header of a for-loop does not mean that it exists only for the life of the for-loop, as you will notice if you do the following:

for (var i = 0; i < 10; i++) { /*...*/ }

console.log(i); //=> 10

// `i` is already declared and its value will be 10
for (; i < 20; i++) { /*...*/ }

console.log(i); //=> 20

You can avoid this problem altogether if you use Array#forEach, which does the job of filesToLoad[i] for you by giving you the next value within a callback function on each iteration:

filesToLoad.forEach((file) => {
  promiseArray.push(
    new Promise( function(resolve, reject ) {
      setTimeout( function() {
        resolve( file );
      }, Math.random() * 1000 );
    })
  );
});

______

Does using either let or var affect the behaviour of Promise#all?

No. In your example, the position of the Promises in promiseArray defines in what order the values are added to the results array, not when each of those Promises is resolved. The fact that you resolve the Promises at random intervals does not move the position of the resolved value within promiseArray. What you have demonstrated is that Promise#all produces an array of values whose positions are mapped to the Promise that produced their value.

See this answer for more information about the behaviour of Promise#all:

All this means that the output is strictly ordered as the input as long as the input is strictly ordered (for example, an array).

sdgluck
  • 24,894
  • 8
  • 75
  • 90
  • 1
    Your example can be even more concise with `Array.prototype.map`: `const promiseArray = filesToLoad.map(file => new Promise(/**/));` – Paolo Moretti Dec 03 '15 at 12:23
  • @PaoloMoretti Thanks :) I favoured familiarity with the original example over introducing new concepts for the sake of OP's understanding. (But you're absolutely right!) – sdgluck Dec 03 '15 at 12:27
1

It's because var's scope isn't a child of for, it's a sibling. So the loop runs once and sets i = 0. It then runs another 2 more times and sets i = 1 and then i = 2. After this has all happened, the timeouts then run and all run the resolve function and passes in resolve( filesToLoad[2] ). let works correctly because the value of let i is not overridden by the following loop iterations.

In short, the timeout only runs after the loop has already run 3 times and therefore passes in the same value. I've created a jsfiddle of a working version using var.

Jamy
  • 901
  • 8
  • 12
0

That's the difference between let and var. Your issue is not directly related to Promises (besides the fact that they run async).


TL;DR

var declarations are moved to beginning of function by interpreter, so when timeouts executed the loop has already run to end of array on the same variable. let keeps the variable in scope, so each iteration of the loop used a different variable.


Background:

Javascript has a feature called hoisting, which means that a variable or function is always declared at the beginning of it's wrapping function. If the coder doesn't, the interpreter moves it. Hence:

var i;
for (i in array)
....

and

for (var i in array)
....

are equivalent, even though, having some reason, or other programming background, you would expect i to be scoped to the loop alone in the second case.

That's why

alert(i);
...
var i=5;

alerts undefined instead of throwing an error. As far as the interpreter is concerned - there was a declaration. It's like

var i;
alert(i);
...
i=5;

Now:

ES6/ES2015 has introduced let - which behaves as our first assumption in the second example above.

alert(i);
...
let i=5;

will actually throw an error. When reaching the alert there is no i to speak of.


In your case

With let you went through the loop with a new variable each time, since let was defined only to scope of the loop. Each iteration is a scope, each used a different variable (though all named i in their respective scopes)

When using var, you actually declared i before the loop

var i;
for( i in filesToLoad ) {
  promiseArray.push(
    new Promise( function(resolve, reject ) {
      setTimeout( function() {
        resolve( filesToLoad[i] );
      }, Math.random() * 1000 );
    })
  );
}

so it's the same variable on each iteration (or, in each scope), meaning that the loop assigned the last item to i before any of the timeouts returned. That's why the result was 3 times the last item.

JNF
  • 3,696
  • 3
  • 31
  • 64
0

These are actually two different, unrelated problems.

Your first observation, that the output is in the original order, rather than a random order, is due to the behaviour of Promise.all. Promise.all returns a new promise that resolves to an array when all sub-promises have resolved, in the original order. You can think of it like Array.map, but for promises.

As stated in the Bluebird (a JS promise library) documentation:

The promise's fulfillment value is an array with fulfillment values at respective positions to the original array.

Your second problem, how changing let to var changes the output to have all of the same values, is due to array scoping. Put simply, a variable defined with var exists outside of the for loop scope. This means that on each iteration, you are actually modifying the value of the pre-existing variable, as opposed to creating a new variable. Since your code inside the Promise executes later, the variable will have the value from the last loop iteration.

let creates a variable that exists inside that for iteration. This means that, later on in the Promise, you are accessing a variable defined in that iterations scope, which isn't overridden by the next loop iteration.

tomb
  • 1,817
  • 4
  • 21
  • 40