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
Correct. You can verify that like this:
Output:
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 aftersetTimeout
completes.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 thePromise
it returned. So the microtask associated with thePromise
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:
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".