3

I was looking into async behaviour in JS and it was going well for the most part. I understand the synchronous way of executing code, the single thread of JS and how callbacks such as the one inside setTimeout will be timed by the Web browser API, and later on added to the task queue.

The event loop will constantly check the call stack, and only when it is empty (all sync code has executed), it will take functions that have been queued in the task queue. Pushes them back to the call stack and they are executed.

This is pretty straight forward and is the reason why following code:

console.log('start');
setTimeout(() => console.log('timeout'), 0);
console.log('end');

Will output start, end, timeout.

Now when I started reading about promises, I understood that they have higher priority than regular async code such as timeout, interval, eventlistener and instead will get placed in the job queue/microtask queue. The event loop will first prioritize that queue and run all jobs until exhaustion, before moving on to the task queue.

This still makes sense and can be seen by running:

console.log('start');
setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => console.log('promise'));
console.log('end');

This outputs start, end, promise, timeout. Synchronous code executes, the then callback gets pushed to the stack from the microtask queue and executed, setTimeout callback task from the task queue gets pushed and executed. All good so far.

I can wrap my head around the example above where the promise gets resolved immediately and synchronously, as told by the official documentation. The same would happen if we were to create a promise with the new keyword and provide an executor function. That executor function will execute synchronously and resolve the function. So when then is encountered, it can just run asynchronously on the resolved promise.

console.log('start');

const p1 = new Promise(resolve => {
    console.log('promise 1 log');
    resolve('promise 1');
});

p1.then(msg => console.log(msg));

console.log('end');

The snippet above will output start, promise 1 log, end, promise 1 proving that the executor runs synchronously.

And this is where i get confused with promises, let's say we have the following code:

console.log('start');

const p1 = new Promise(resolve => {
    console.log('promise 1 log');
    setTimeout(() => {
        resolve('promise 1');
    }, 0);
});

p1.then(msg => console.log(msg));

console.log('end');

This will result in start, promise 1 log, end, promise 1. If the executor function gets executed right away, that means that the setTimeout within it will get put on the task queue for later execution. To my understanding, this means the promise is still pending right now. We get to the then method and the callback within it. This will be put in the job queue. the rest of the synchronous code is executed and we now have the empty call stack.

To my understanding, the promise callback will have the priority now but how can it execute with the still unresolved promised? The promise should only resolve after the setTimeout within it is executed, which still lies inside the task queue. I have heard, without any extra clarification that then will only run if the promise is resolved, and from my output i can see that's true, but i do not understand how that would work in this case. The only thing i can think of is an exception or something similar, and a task queue task getting the priority before the microtask.

This ended up being long so i appreciate anyone taking the time to read and answer this. I would love to understand the task queue, job queue and event loop better so do not hesitate posting a detailed answer! Thank you in advance.

  • And, keep in mind that most of the time this relative priority of different kind of asynchronous events is NOT something you should rely on in your coding because all of these are asynchronous races anyway with unpredictable timing. If you want a particular response to be processed before another response, you should write your code to actually do that without regard for this level of timing minutiae by forcing certain sequencing in the way you write your code. – jfriend00 Aug 05 '20 at 04:34
  • That makes a lot of sense, thanks for the reminder. I am still pretty new to these async concepts and want to understand them well. I can only imagine that when working with data or functions that may take an unpredictable amount of time, it's better to just force their order in the way you want to in a sure way. – Konstantinos Pascal Aug 05 '20 at 04:47

3 Answers3

3

We get to the then method and the callback within it. This will be put in the job queue.

No, calling then doesn't put anything in the job queue immediately if the promise is still pending. The callback will be installed on the promise for execution later when the promise is fulfilled, just like an event handler. Only when you call resolve(), it actually puts it in the job queue.

This works just like the setTimeout, where you wrote "[the] callback […] will be timed by the Web browser API, and later on added to the task queue" - it doesn't immediately queue a task that somehow waits, but it waits and then queues a task to execute the callback.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Is there any way to block the code on line `p1.then(msg => console.log(msg));` ? – variable Feb 18 '22 at 11:52
  • @variable What code do you want to block? You cannot block *all* code. You can only write `const msg = await p1; console.log(msg); …` in an `async` function – Bergi Feb 18 '22 at 13:51
1

... the promise callback will have the priority now ...

Tasks in the microtask queue are given priority over those in the task queue only when they exist.

In the example :

  • No microtask is queued until after the setTimout() task has resolved the Promise.
  • The task and microtask are not in competition. They are sequential.
  • Delays imposed by the task queue and microtask queue (in that order) are additive.

... but how can it execute with the still unresolved promised?

It doesn't. The .then() callback will execute only after the promise is fulfilled, and that fulfillment is dependent on a task placed in the task queue by setTimeout() (even with a delay of zero).

Roamer-1888
  • 19,138
  • 5
  • 33
  • 44
  • With an empty call stack and tasks in both the task queue and the job queue, the event loop will first pick up the function from the job queue to push to the call stack, isn't that correct? That's what i meant by priority. But as you said, then() is dependent on a task placed in the task queue. Will that fact make the event loop go and first push that task from the task queue before getting back to the then callback that is placed in the job queue? – Konstantinos Pascal Aug 05 '20 at 04:19
  • 2
    Your misunderstanding appears to be that `promise.then(callback)` places a task in the microtask queue. It doesn't. Rather, it holds `callback` in abeyance until `promise` is fulfilled. Only then will it make an entry in the microtask queue. – Roamer-1888 Aug 05 '20 at 04:31
  • 1
    Oooh, that makes a lot of sense. By adding an additional log inside the timeout, I saw that the timeout cb did in fact run before my then cb, and as you said, that would be because there were no microtasks to be picked up. I then assumed that the resolve function would place the microtask, but your answer cleared that up. – Konstantinos Pascal Aug 05 '20 at 04:38
  • Whew! I thought I was in for a lengthy one :-) – Roamer-1888 Aug 05 '20 at 04:43
  • Yeah, I get it now. I would be interested to know how the callback inside then is being held in abeyance until the promise is resolved, in slight more detail, if you don't mind! – Konstantinos Pascal Aug 05 '20 at 04:50
  • 2
    @KonstantinosPascal - The promise stores internally an array of listeners for the resolve and reject notifications. The appropriate listeners (callbacks) are then inserted into the task queue when and only when the promise becomes resolved or rejected and thus those listeners should be called. It is very much like event listeners on an `EventEmitter` except these listeners can only ever be called once (per Promise design specs). – jfriend00 Aug 05 '20 at 04:56
  • I can't add anything to @jfriend00's explanation. – Roamer-1888 Aug 05 '20 at 17:32
  • Is there any way to block the code on line `p1.then(msg => console.log(msg));` ? – variable Feb 18 '22 at 12:00
  • @variable, it depends what you mean by "block". Three distinct cases ... (1) when the chain is built, you can choose to chain `.then()` or not; ... (2) if `p1` rejects then it will take its error path, bypass then's success handler and proceed to the next error handler in the chain ... (3) when the chain settles, the callback can contain any number of conditions/branches to control its behavoiur. Or some combination of all three. – Roamer-1888 Feb 18 '22 at 17:51
0

JS engine has got 1 call stack, macro task queue, micro task queue and web api's. More about basic concept: https://stackoverflow.com/a/30910084/1779091

In case of Promise, code inside the promise will run and when resolve is called, then the callback gets added into the micro queue.

Whereas setTimeout runs in the web api and once it completes, it puts the callback into the macro queue.

console.log('start');
setTimeout(() => console.log('timeout'), 0);
console.log('end');
  • Print start
  • Call the setTimeout web api and pass the call back into it
  • At this point the setTimeout may or may not have already completed. Whenever timer is exhausted, the callback will be put into the macro queue
  • Print end
  • Nothing left to execute, so check if the queue has something. This will output the timeout.
console.log('start');
setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => console.log('promise'));
console.log('end');
  • Print start
  • Call the setTimeout web api and pass the call back into it
  • At this point the setTimeout may or may not have already completed. Whenever timer is exhausted, the callback will be put into the macro queue
  • Promise is resolved which puts the callback (the thing after .then) into the micro task queue
  • Print end
  • Nothing left to execute, so check if the queue has something. Micro task queue has higher priority then macro task queue. So 1st it will take the callback from micro task into the call stack and print promise and then take the callback from the macro task queue into the call stack and print timeout.
console.log('start');

const p1 = new Promise(resolve => {
    console.log('promise 1 log');
    resolve('promise 1');
});

p1.then(msg => console.log(msg));

console.log('end');
  • Print start
  • Create the promise and assign it to p1
  • Run the p1 which prints promise 1 log, then resolve which puts the callback (the thing after .then) into the micro task queue
  • Print end
  • Nothing left to execute, so check if the queue has something. The callback from the Micro task is put into the stack and it will print promise 1
console.log('start');

const p1 = new Promise(resolve => {
    console.log('promise 1 log');
    setTimeout(() => {
        resolve('promise 1');
    }, 0);
});

p1.then(msg => console.log(msg));

console.log('end');
  • Print start
  • Create the promise and assign it to p1
  • Run the p1 which prints promise 1 log, then calls setTimeout which invokes the web api. At this point the setTimeout may or may not have already completed. Whenever timer is exhausted, the callback will be put into the macro queue
  • Print end
  • Nothing left to execute, so check if the queue has something. The callback from the Macro task is put into the stack and it will run the resolve which puts the callback (the thing after .then) into the Micro task queue.
  • Nothing left to execute, so check if the queue has something. The callback from the Micro task is put into the stack and it will print promise 1
variable
  • 8,262
  • 9
  • 95
  • 215
  • For more detail, you might want to distinguish between "*the promise is created (and the executor is executed)*" and "*`.then()` is called on the promise to install/schedule the handler (and create a new promise)*", especially if they are put on separate lines in the examples. – Bergi Feb 18 '22 at 13:56
  • `.then()` results in a new Promise? – variable Feb 18 '22 at 20:04
  • 1
    Yes, otherwise you couldn't chain them – Bergi Feb 18 '22 at 23:43
  • I didn't really understand your point, it will be nice if you can elaborate. – variable May 26 '22 at 13:01