Search code examples
javascriptasynchronouspromiseevent-loopjob-queue

Understanding async JS with promises, task and job queue


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.


Solution

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