3

Say I have a bunch of promises.

const promises = [ /*...*/ ];

And I want something to happen when all the promises have resolved. What's the difference between the following two ways of doing this:

  1. Using Promise.all

    const allDonePromise = Promise.all(promises);
    
  2. Running the promises serially

    async function allPromises(promisesArray) {
      const results = [];
    
      for (const promise of promisesArray) {
        results.push(await promise);
      }
    
      return results;
    }
    
    const allDonePromises = allPromises(promises);
    

Is Promise.all just a builtin which does what allPromises does or is there something else happening under the hood to make Promise.all faster. Where can I find information about the internals of Promise.all?

0xcaff
  • 13,085
  • 5
  • 47
  • 55
  • Related: https://stackoverflow.com/questions/45285129/any-difference-between-await-promise-all-and-multiple-await – 0xcaff Aug 19 '17 at 01:23
  • The related question doesn't ask about performance and the internal differences. – 0xcaff Aug 19 '17 at 01:23
  • Promise.all allows all promises to run "at once" - chaining promises runs them in series ... your `allPromises` function however is not actually chaining the promises at all because all the promises are executed "at once" anyway – Jaromanda X Aug 19 '17 at 01:27
  • 1
    @JaromandaX Doesn't the promise executor start running as soon as the Promise constructor is called? – 0xcaff Aug 19 '17 at 01:29
  • Isn't the `allPromises` code equivalent to `await promise1; await promise2; await promise3;` which is equivalent to `promise1.then(() => promise2).then(() => promise3);`? – 0xcaff Aug 19 '17 at 01:32
  • @JaromandaX My code would also reject on the first error. An exception would be thrown and it would be converted to a Promise rejection because `async`. – 0xcaff Aug 19 '17 at 01:33
  • @JaromandaX Does the JavaScript runtime handle it the same way? – 0xcaff Aug 19 '17 at 01:35
  • `Where can I find information about the internals of Promise.all` - here's one example - https://jsfiddle.net/jf5Lx6d7/ – Jaromanda X Aug 19 '17 at 01:42

1 Answers1

6

You can find the specifications of Promise.all in section 25.4.4.1 of the ECMAScript 2015 Language Specification.

Your own implementation is indeed doing the right thing. The differences are in details:

The above specs state at point 25.4.4.1.1.r that then is to be called on each of the promises. These calls happen synchronously (NB: not their callbacks). Whenever any of the promises resolves, a remainingElementsCount is decremented (see step 2.10). Whenever it gets to zero, the promise that was returned by Promise.all is resolved (NB: synchronously!).

Now imagine you have an array of a million promises, and the first one takes longest to resolve, then your function will still have to perform 999999 awaits before the function returns, while the algorithm in the specs would already have processed the resolutions of those 999999 promises before the first one resolved, and will have little to do after the first promise finally resolves.

You can see this for instance in this polyfill/promise.js implementation (where the counting happens by incrementing):

shaka.polyfill.Promise.all = function(others) {
  var p = new shaka.polyfill.Promise();
  if (!others.length) {
    p.resolve_([]);
    return p;
  }
  // The array of results must be in the same order as the array of Promises
  // passed to all().  So we pre-allocate the array and keep a count of how
  // many have resolved.  Only when all have resolved is the returned Promise
  // itself resolved.
  var count = 0;
  var values = new Array(others.length);
  var resolve = function(p, i, newValue) {
    shaka.asserts.assert(p.state_ != shaka.polyfill.Promise.State.RESOLVED);
    // If one of the Promises in the array was rejected, this Promise was
    // rejected and new values are ignored.  In such a case, the values array
    // and its contents continue to be alive in memory until all of the Promises
    // in the array have completed.
    if (p.state_ == shaka.polyfill.Promise.State.PENDING) {
      values[i] = newValue;
      count++;
      if (count == values.length) {
        p.resolve_(values);
      }
    }
  };
  var reject = p.reject_.bind(p);
  for (var i = 0; i < others.length; ++i) {
    if (others[i].then) {
      others[i].then(resolve.bind(null, p, i), reject);
    } else {
      resolve(p, i, others[i]);
    }
  }
  return p;
};

But be aware that browser implementations differ. The above polyfill is just one of the possible implementations.

Note that your function is not "Running the promises serially". The promises are "running"* whether you do something with them or not: they do their job as soon as you construct them.

The only things that get serialised are the moments you start to look at (i.e. await) the respective promise resolutions. The specs seem to hint that the implementation should listen to resolve callbacks of all promises from the start. This you cannot implement with await in a loop (well, you could, but then you would need to call the async function repeatedly, once per promise, which would not give you any benefit any more of using await over then, as you would need to apply then on the promises returned by the async function).

Then there are some other (obvious) differences in the area of the this and argument validation. Notably the ECMA specs state:

The all function requires its this value to be a constructor function that supports the parameter conventions of the Promise constructor.


* Promises don't really "run", as promises are objects, not functions. What may be running is some asynchronous task that was initiated when the promise object was created. The promise constructor callback can potentially call an asynchronous API (like setTimeout, fetch, ...) which may lead to an asynchronous call of resolve. It is better to call this intermediate state as a promise that is "pending" (instead of "running")

trincot
  • 317,000
  • 35
  • 244
  • 286