When it's time to call a promise callback, the job doesn't go on the standard job queue (ScriptJobs) at all; it goes on the PromiseJobs queue. The PromiseJobs queue is processed until it's empty when each job from the ScriptJobs queue ends. (More in the spec: Jobs and Job Queues.)
I'm not sure what output you were expecting from your code as you didn't say, but let's take a simpler example:
console.log("top");
new Promise(resolve => {
setTimeout(() => {
console.log("timer callback");
}, 0);
resolve();
})
.then(() => {
console.log("then callback 1");
})
.then(() => {
console.log("then callback 2");
});
console.log("bottom");
The output of that, reliably, is:
top
bottom
then callback 1
then callback 2
timer callback
because:
- The ScriptJobs job to run that script runs
console.log("top") runs
- The promise executor function code runs, which
- Schedules a timer job for "right now," which will go on the ScriptJobs queue either immediately or very nearly immediately
- Fulfills the promise (which means the promise is resolved before
then is called on it) by calling resolve with no argument (which is effectively like calling it with undefined, which not being thenable triggers fulfillment of the promise).
- The first
then hooks up the first fulfillment handler, queuing a PromiseJobs job because the promise is already fulfilled
- The second
then hooks up the second fulfillment handler (doesn't queue a job, waits for the promise from the first then)
console.log("bottom") runs
- The current ScriptJob job ends
- The engine processes the PromiseJobs job that's waiting (the first fulfillment handler)
- That outputs
"then callback 1" and fulfills the first then's promise (by returning)
- That queues another job on the PromiseJobs queue for the callback to the second fulfillment handler
- Since the PromiseJobs queue isn't empty, the next PromiseJob is picked up and run
- The second fulfillment handler outputs
"then callback 2"
- PromsieJobs is empty, so the engine picks up the next ScriptJob
- That ScriptJob processes the timer callback and outputs
"timer callback"
In the HTML spec they use slightly different terminology: "task" (or "macrotask") for ScriptJobs jobs and "microtask" for PromiseJobs jobs (and other similar jobs).
The key point is: All PromiseJobs queued during a ScriptJob are processed when that ScriptJob completes, and that includes any PromiseJobs they queue; only once PromiseJobs is empty is the next ScriptJob run.