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.