skip to Main Content

I have this bit of code:

setTimeout(
  () => console.log("setTimeout callback is complete"),
  0);

fetch("https://api.coingecko.com/api/v3/ping")
  .then(() => console.log("fetch callback is complete"));

// busy-wait for a few seconds:
for (let i = 0; i < 1000000; i++) {
  for (let j = 0; j < 30000; j++) {
  }
}

console.log("busy-wait is complete");

I was expecting this output:

busy-wait is complete
fetch callback is complete
setTimeout callback is complete

because microtasks (such as promise outputs) have higher priority than macrotasks (such as setTimeout timers); but when I try it in Chrome, I actually get this output:

busy-wait is complete
setTimeout callback is complete
fetch callback is complete

(Note: I’m expecting the fetch call to be pretty quick; it should finish before the busy-wait is complete.)

When I increase setTimeout’s timer duration to 1 ms, it outputs what I expected.

setTimeout(
  () => console.log("setTimeout callback is complete"),
  1); // <--- this is the change

fetch("https://api.coingecko.com/api/v3/ping")
  .then(() => console.log("fetch callback is complete"));

// busy-wait for a few seconds:
for (let i = 0; i < 1000000; i++) {
  for (let j = 0; j < 30000; j++) {
  }
}

console.log("busy-wait is complete");

My question is, what’s changed after I increase the setTimeout duration, even to just 1 ms? Why does it evaluate the two situations differently, and ignore the fetch priority when the timer duration is 0 ms?

2

Answers


  1. microtasks (such as promise outputs) have higher priority than macrotasks (such as setTimeout timers)

    Correct. You can verify that like this:

    setTimeout(() => console.log("macrotask (setTimeout)"), 0)
    Promise.resolve().then(() => console.log("microtask (promise)"))
    console.log("sync")

    Output:

    sync
    microtask (promise)
    macrotask (setTimeout)
    

    However, what you’re doing is not just queueing an immediately resolved promise, but rather sending a fetch request. The request is sent immediately as a microtask, but the response arrives back after setTimeout completes.

    Login or Signup to reply.
  2. because microtasks (such as promise outputs) have higher priority than macrotasks (such as setTimeout timers);

    No, they don’t have higher priority. They are not part of the task prioritization system at all. Once a microtask is queued, it will get executed at the next microtask checkpoint, which is generally when the current JS script ends execution as part of clean after running a script.
    So even between two callbacks that are not tasks1 like two requestAnimationFrame callbacks fired in the same animation frame, queued microtasks will get executed.

    However, fetch() will actually queue a fetch task1 that will itself resolve the Promise it returned. So the microtask associated with the Promise resolution will actually get queued from that fetch task.

    And that’s where the prioritization system comes in. At the end of your busy loop we have in the timer task source our setTimeout task, and in the networking task source, our fetch task.
    With this, we can’t assume anything about the order in which these two tasks will get executed. The specs let the user agent decide whatever they deem is best with the step 2.1 of the event loop processing:

    Let taskQueue be one such task queue, chosen in an implementation-defined manner.

    Browsers do use many heuristics to choose between the various task queues, and while it’s generally agreed that UI tasks will get higher priority than others, between network and timer it’s a lot less clear. Also given that Chrome only recently removed a 1ms clamping to their setTimeout() implementation, I wouldn’t even consider the current behavior as being stable, they may very well decide to deprioritize even 0ms timers at some point in the future, and they’d still be specs compliant.

    If one wants to queue tasks with a given priority, then there is an incoming Prioritized postTask API that does that, but it’s still not widely supported (Chrome has it unflagged, Firefox had a weird implementation at some point, and I’m not sure Safari really followed there just yet).

    1. Note that "macrotask" isn’t a thing, there are only "tasks" and "microtasks".

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search