javascriptpromiseevent-loopasynchronous-javascriptruntime-environment

Nested asynchronous javascript (microtask and macrotasks queues)


Recently I've been given a following problem:

console.log("start");

const promise1 = Promise.resolve().then(() => {
  console.log("promise1");
  const timer2 = setTimeout(() => {
    console.log("timer2");
  }, 0);
});

const timer1 = setTimeout(() => {
  console.log("timer1");
  const promise2 = Promise.resolve().then(() => {
    console.log("promise2");
  });
}, 0);

console.log("end");

My initial answer was start, end, promise1, timer2, promise2, timer1 (wrong one).

I understand that synchronous code executes first then microtasks and then macrotasks. I also understand that microtask may appear between two macrotasks. I'm slightly confused and not fully aware why timer2 wasn't queued by macrotask when we were executing promise1 in a callstack. I also understand that JS engine read and queued timer1 while we were processing promise1, but can't fully understand the reason for it.

To me it seems like globally declared values are read first.

Below is the principle that solves the order of a problem:

  1. First, all micro tasks are performed. (I assume this means all global microtasks first.)
  2. One macro task is performed.
  3. All (re-added) micro-tasks are re-tasks.
  4. The following macro problem is executed.
  5. Cycle repeats / Cycle ends.

By this principle, it seems that global values are considered first. I understand the logic behind the answer, but can't find any answers on whether "global values are considered first statement" is true. I'm not sure why timer2 wasn't queued when we processed promise1. I somewhat believe it was queued but timer1 was processed faster than timer2 since it is declared globally . Was timer1 read first and queued before timer2 because it is in global scope? What is the reason for it? I somewhat understand it but not entirely sure.


Solution

  • but can't find any answers on whether "global values are considered first statement" is true. I'm not sure why timer2 wasn't queued when we processed promise1. I somewhat believe it was queued but timer1 was processed faster than timer2 since it is declared globally . Was timer1 read first and queued before timer2 because it is in global scope?

    It's not really about scope, but order of execution. As you mentioned, synchronous code happens first, and part of that synchronous code is to set up timer1. Setting up timer2 is not part of the synchronous code; it happens later.

    Here's the sequence that happens. First, the synchronous stuff:

    1. Log "start"
    2. Create a resolved promise, and call .then on it to make promise1.
    3. Start timer 1. Ie, request the browser that it should queue up a macrotask some time in the future.
    4. Log "end"

    Now that that's done, code execution returns. There is one microtask waiting to run (the .then related to promise1), so it runs next.

    1. Log "promise1"
    2. Start timer 2. Ie, request the browser that it should queue up a macrotask some time in the future.

    Now that's done, and there's nothing else to be done for a little while. After the minimum timer delay elapses, timers start going off. I'm not sure if the 2 tasks get inserted into the queue at exactly the same time or slightly separated in time. But the order of the tasks will always be the same (same order they were created, given they have the same delay), so it won't affect the output that's logged. Let's assume both macrotasks get queued simultaneously. Thus we have 2 macro tasks in the queue, and it handles the first one, which is for timer1:

    1. Logs "timer1"
    2. Create a resolved promise, and call .then on it to create promise2

    When this returns, there is one microtask waiting to run (the .then related to promise2) and one macrotask (timer 2). The microtask runs first:

    1. Logs "promise2"

    When this returns, there is one macrotask waiting to run, so that runs next:

    1. Logs "timer2"

    Was timer1 read first and queued before timer2 because it is in global scope?

    It's not really scope related, it's just that creating timer1 is part of the synchronous code, and creating timer2 only happens later.