Understanding async JS with promises, task and job queue

Viewed 1200

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.

3 Answers

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.

... 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).

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
Related