3

The difference between Code#1 and Code#2 is: Code#1 uses resolve(p) and Code#2 uses p.then(()=>resolve()). I would expect the sequence of output to be invariant, but they generate a different sequence. I cannot figure out why.

Code #1: resolve(p)

const p = Promise.resolve();

new Promise((resolve) => {
    resolve(p);    // <---
}).then(() => {
    console.log('after:await');
});

p.then(() => console.log('tick:a'))
    .then(() => console.log('tick:b'))
    .then(() => console.log('tick:c'));

Output:

tick:a
tick:b
after:await
tick:c

Code #2: p.then(()=>resolve())

const p = Promise.resolve();

new Promise((resolve) => {
    p.then(()=>resolve());    // <---
}).then(() => {
    console.log('after:await');
});

p.then(() => console.log('tick:a'))
    .then(() => console.log('tick:b'))
    .then(() => console.log('tick:c'));

Output:

tick:a
after:await
tick:b
tick:c

Why is the order of output different?

trincot
  • 317,000
  • 35
  • 244
  • 286
PunCha
  • 258
  • 2
  • 11
  • 2
    When there are multiple Promise objects in play and ready for dispatch, the runtime can interleave the jobs however it sees fit. For any *single* Promise the callbacks have to be called in order, but callbacks for other Promise instances can be interleaved (or not). – Pointy Jan 15 '19 at 16:10
  • 3
    `resolve` resolves a promise, `then` attaches a function to be executed after the promise is resolved. They're quite different. – deceze Jan 15 '19 at 16:11
  • @Pointy, it seems that although the Promise/A+ specs allow for interleaving the Promise-jobs freely, EcmaScript specs define a deterministic order: jobs are queued FIFO. – trincot Jan 16 '19 at 14:56
  • @trincot yes, I think you're right; the spec is confusing but it seems that the job queue for promises is always the `"PromiseJobs"` queue, implying that all the `.then()` etc jobs go into one big queue. I was confused by the language that says jobs from *other* job queues may be interleaved. – Pointy Jan 16 '19 at 15:07
  • 2
    I wouldn't deliberately design code that *did* rely on ordering of promise callbacks, but I suppose it'd be maddening if it really were unpredictable because inadvertent dependencies would cause hard-to-reproduce bugs. – Pointy Jan 16 '19 at 15:08

4 Answers4

6

This is in fact a very interesting question, because the Promise/A+ specs would allow the first code version to produce the same output as the second version of the code.

One could dismiss the question saying the Promise implementation says nothing about how resolve(p) would be implemented. This is a true statement when looking at the Promise/A+ specification, quoting from its preface:

the core Promises/A+ specification does not deal with how to create, fulfill, or reject promises, ...

But the EcmaScript specification for Promises (Section 25.4) is quite more detailed than the Promise/A+ specification and requires that "jobs" are added to the back of the relevant job queue -- which for promise settlements is the PromiseJobs queue (25.4.1.3.2 and 8.4): this determines a specific order:

Required Job Queues

[...]
PromiseJobs: Jobs that are responses to the settlement of a Promise

[...]

The PendingJob records from a single Job Queue are always initiated in FIFO order

It also defines that resolve(p) -- when p is a thenable -- will first put a job on the queue that will perform the necessary internal call of the p.then method. This is not done immediately. To quote the note in the EcmaScript specs at 25.4.2.2:

This process must take place as a Job to ensure that the evaluation of the then method occurs after evaluation of any surrounding code has completed.

This statement is illustrated with the order of output in the following snippet:

const p1 = Promise.resolve();

// Wrap the `p1.then` method, so we can log something to the console:
const origThen = p1.then;
p1.then = function(...args) {
    console.log("The p1.then method is called asynchronously when triggered by resolve(p1)");
    origThen.call(this, ...args);
};
const p2 = new Promise(resolve => {
    resolve(p1);
    console.log("Code that follows is executed synchronously, before p1.then is");
});

When we use the p1.then(resolve) method call instead of resolve(p1), we get the opposite order:

const p1 = Promise.resolve();

// Wrap the `p1.then` method, so we can log something to the console:
const origThen = p1.then;
p1.then = function(...args) {
    console.log("The p1.then method is called synchronously now");
    origThen.call(this, ...args);
};
const p2 = new Promise(resolve => {
    p1.then(resolve);
    console.log("Code that follows is executed synchronously, after p1.then is");
});

Your code

The above really explains the different order of output you get. Here is how the first code version sequences the actions. First let me rewrite this a bit, so that most involved promises have a name:

const p1 = Promise.resolve();
const p2 = new Promise((resolve) => resolve(p1));
const p3 = p2.then(() => console.log('after:await'));
const p4 = p1.then(() => console.log('tick:a'));
const p5 = p4.then(() => console.log('tick:b'))
const p6 = p5.then(() => console.log('tick:c'));

Now, after the main, synchronous code has executed to completion, only p1 has a resolved state, and two jobs are present on the job queue (micro tasks queue), one as a result of resolve(p1) and a second one because of p1.then:

  1. According to 25.4.2.2, the then method of p1 is called passing it the internal [[resolve]] function related to p2. The p1.then internals know that p1 is resolved and put yet another job on the queue to actually resolve p2!

  2. The callback with "tick:a" is executed, and promise p4 is marked as fulfilled, adding a new job in the job queue. There are now 2 new jobs in the queue, which are processed in sequence:

  3. The job from step 1 is executed: p2 is now resolved. This means a new job is queued to actually call the corresponding then callback(s)

  4. The job from step 2 is executed: the callback with "tick:b" is executed

Only later the job added in step 3 will be executed, which will call the callback with "after:await".

So, in conclusion. In EcmaScript a resolve(p), where p is a thenable involves an asynchronous job, which itself triggers yet another asynchronous job to notify the fulfilment.

The then callback, that differentiates the second code version, will only need one asynchronous job to get called, and thus it happens before the output of "tick:b".

Community
  • 1
  • 1
trincot
  • 317,000
  • 35
  • 244
  • 286
  • 1
    Personally I think it was a bad decision to dictate this behaviour in the spec, they should've left it up to the implementations. In any case, this was made only so that all implementations are *consistent* in their interleaving of concurrent tasks, and not that anyone should try to rely on this particular order in his code. – Bergi Jan 16 '19 at 16:03
  • @Bergi while I agree it was an unfortunate decision, it does make consumer's life easier by guaranteeing an order for a particular sequence (assuming the implementation is compliant, which is a different issue entirely). I believe an analogous situation exists where `Array.prototype.sort()` could have been spec'd as stable or simply implementation-dependent, and while I agree with the specification's choice to allow stability to be implementation dependent, there are a lot of code bases that rely on implementations choosing a stable sort algorithm. This was a hot topic in V8 for a while. – Patrick Roberts Sep 19 '19 at 14:56
  • @PatrickRoberts Sure, but while a stable sort is a reasonable thing to expect, no code base should ever depend on the particular sequence of concurrent promises. Specifying such details did require them to update the spec a few times now, where e.g. V8 did optimise away an unnecessary tick on their `await` implementation. (And nothing broke). – Bergi Sep 19 '19 at 16:26
2

In both your answers promise chain1 and promise chain2 can be interleaved differently. But, tick:a, tick:b, tick:c will be outputted in that order, tick:a before tick:b, and tick:b before tick:c. after:await can be outputted anywhere in between.

For what your code is doing.

// Returns a resolved promise object
// Which is equivalent to const p = new Promise(resolve => resolve());
const p = Promise.resolve();

// For Reference Call This Promise Chain 1

new Promise((resolve) => {
    // Fulfills the promise with the promise object p 
    resolve(p);  // (1)
}).then(() => {
    console.log('after:await');
});

For Reference Promise Chain 2
p.then(() => console.log('tick:a'))
    .then(() => console.log('tick:b'))
    .then(() => console.log('tick:c'));
const p = Promise.resolve();

new Promise((resolve) => {
    // Here you are calling then which if promise p has been fulfilled
    // will call the callback you passed as an argument, which then
    // will eventually cause the outer promise to enter a state of
    // fulfilled triggering a call to the next 'then' provided in the part of the chain. 
    p.then(()=>resolve());
}).then(() => {
    console.log('after:await');
});


p.then(() => console.log('tick:a'))
    .then(() => console.log('tick:b'))
    .then(() => console.log('tick:c'));

Mike
  • 587
  • 3
  • 6
  • *"after:await can be outputted anywhere in between."*: it turns out that the EcmaScript specification is dictating a specific order, leaving no room for other output orders than how it is working in the Asker's code. – trincot Jan 16 '19 at 12:53
1

The Promise.resolve() method returns a Promise object that is resolved with a given value. If the value is a promise, that promise is returned; if the value is a thenable (i.e. has a "then" method), the returned promise will "follow" that thenable, adopting its eventual state; otherwise the returned promise will be fulfilled with the value. This function flattens nested layers of promise-like objects (e.g. a promise that resolves to a promise that resolves to something) into a single layer.

Please refer here for more info about Promise.resolve().

The difference in the output of both of your codes is due to the fact that the then handlers are called asynchronously.

When using a resolved promise, the 'then' block will be triggered instantly, but its handlers will be triggered asynchronously.

Please refer here for more info about then handlers' behaviour.

Ashutosh Pandey
  • 197
  • 3
  • 13
  • May I suggest you format the first paragraph as a quote, since it is literal copy of the MDN article you refer to. Secondly you write *"The difference in the output of both of your codes is due to the fact that the then handlers are called asynchronously."*. But that does not explain the difference in the output. Both codes have this asynchronous effect. In fact, the first code has one more `then`, yet the corresponding output comes earlier (not later) than in the second version. – trincot Jan 16 '19 at 12:48
0

A Promise is in one of these states:

    -pending: initial state, neither fulfilled nor rejected.
    -fulfilled: meaning that the operation completed successfully.
    -rejected: meaning that the operation failed.
A pending promise can either be fulfilled with a value, or rejected with a reason (error). When either of these options happens, the associated handlers queued up by a promise's then method are called. Refer this for more details

Now in your particular case, you are using "Promise.resolve()" probably to create a new promise object but what it does is that it creates an already resolved promise with no value. So your promise object "p" is resolved during its creation and the rest of the code where you resolve it has literally no effect other than putting the "after:wait" into a handler queue. Please refer to the Event Loop with Zero Delay. The output for both the codes is different based on when the "console.log" is put in the call stack and not because how you are writing it.

The correct way of doing this can be:

var promise1 = new Promise(function(resolve, reject) {
 setTimeout(function() {
  resolve('foo');
 }, 300);
});

promise1.then(function(value) {
 console.log(value);// expected output: "foo"
});

console.log(promise1);// expected output: [object Promise]
Amritansh
  • 172
  • 2
  • 6
  • I don't think the question was to find "the correct way of doing this". It is asking why the output is different. Sure, it depends on when the `console.log` is put in the call stack, but that is really the question: why is it put in a different order in the two code versions? – trincot Jan 16 '19 at 12:51
  • The link [Event Loop with Zero Delay](https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop) is good enough to get the basics straight. One can know more from [here](https://www.youtube.com/watch?v=8aGhZQkoFbQ) – Amritansh Jan 18 '19 at 04:45
  • although those links are helpful to understand the mechanics of asynchrony in JS, I think the OP is quite aware of that. These resources however do not explain the specific difference in sequencing that OP is asking about. – trincot Jan 18 '19 at 08:54