2

I'm running the code below. The output I was expecting was: Start -> End -> While expires -> joke -> setTimeout However, the output I’m actually getting is: Start -> End -> While expires -> setTimeout -> joke Why is this happening? Shouldn’t "joke" be printed before "setTimeout", since fetch callbacks are stored in the microtask queue, which has higher priority than the task queue where setTimeout callbacks are stored?

console.log("Start");

setTimeout(function cb() {
    console.log("setTimeout");
}, 5000);

const apiUrl = "https://api.chucknorris.io/jokes/random";

fetch(apiUrl)
    .then((response) => response.json())
    .then((data) => {
        const joke = data.value;
        console.log(joke);
    })
    .catch((error) => {
        console.error("Error:", error);
    });

console.log("End");

let startDate = new Date().getTime();
let endDate = startDate;
while (endDate < startDate + 10000) {
    endDate = new Date().getTime();
}

console.log("While expires");

3
  • 1
    Can you check how much time the API is taking? Any priority is for when they are entered into the queue. What async operation gets completed before is still going to depend on other factors right. Commented Sep 7 at 11:01
  • 2
    This behaviour can trivially be explained by the http request taking longer than the 5s of the timer, even without the while loop. There is no guarantee in which order those two asynchronous callback run. One of the tasks also involving a .then() microtask is a false lead, there is no fixed priority. Commented Sep 7 at 11:38
  • See also stackoverflow.com/questions/74182571/… Commented Sep 7 at 20:17

1 Answer 1

3

fetch callbacks are stored in the microtask queue, which has higher priority than the task queue where setTimeout callbacks are stored?

If with "fetch callbacks" you mean the promise related jobs that are queued once the response has been received, then yes, this is true, but realise that when the fetch API gets the response, it is the first then() promise that resolves, not yet the second, chained one. So the first then callback will be put in the microtask queue, not yet the second callback that has your console.log call.

That first then callback will indeed execute once the busy loop has ended, and before the setTimeout callback, and it will result in another asynchronous call (json()). At this point in time the microtask queue is empty (in this case: briefly): the json() promise will resolve in a new macrotask, and so the setTimeout callback will execute first. Once the json() promise resolves, the second then callback will be placed in the microtask queue, but at this point the setTimeout callback was already dealt with.

If you would output something in the first then callback this might become clearer:

console.log("Start");

setTimeout(function cb() {
    console.log("setTimeout");
}, 5000);

const apiUrl = "https://api.chucknorris.io/jokes/random";

fetch(apiUrl)
    .then((response) => {
        console.log("Got response. Calling json()...");
        return response.json();
    })
    .then((data) => {
        const joke = data.value;
        console.log(joke);
    })
    .catch((error) => {
        console.error("Error:", error);
    });

console.log("End");

let startDate = new Date().getTime();
let endDate = startDate;
while (endDate < startDate + 6000) {
    endDate = new Date().getTime();
}

console.log("While expires");

The output is now:

Start
End
While expires
Got response. Calling json()...
setTimeout
(joke)
Sign up to request clarification or add additional context in comments.

21 Comments

Please expand on "At this point in time the microtask queue is briefly empty" - why only "briefly"?
Because the joke text is a relatively small payload (not megabytes of data). The json() asynchronous logic is expected to complete quickly.
Yes, quickly, but not immediately. And importantly, it gets a separate macro task that resolves the promise.
I have added this clarification in the answer. Thanks!
"Yes, this is true," nope, or at least it's very misleading to think of it this way. There is simply no priority equivalence between a microtask and a task. They're not part of the same queues (except a very rare exception where a microtask can become a task, but then it's a microtask anymore) and are thus not comparable at all. A timer task and a network task may have different priorities, not a task and a microtask. Queued microtasks are simply ran as soon as the JS engine is free, which might be many times in a single event loop iteration. Only a single task is ran per event loop iteration.
Are you questioning whether fetch callbacks are placed in the microtask queue, or that setTimeout callbacks are placed in the task queue, or whether all microtasks are executed right after the current task empties the call stack, and before the next task? If all of these are true, the order is well defined in my understanding.
I am saying that the quote "the microtask queue, which has higher priority than the task queue where setTimeout callbacks are stored" is not true, contrarily to what your first sentence states. There is no priority relationship between tasks and microtasks, they're not comparable. And it's quite unfortunate in this case since as shown in the duplicate, there is actually a prioritization story in this case. But it's not between tasks and microtasks, and instead between the task(s) queued on the network queue to resolve the promise(s) and the one queued on the timer queue.
I don't understand what you are saying. Surely when a current task empties its call stack, and there are entries in both the task and microtask queue, it will give priority to the ones in the microtask queue. Are you contesting that?
No they're not, in fact. The real point they missed is that fetch() does queue a "normal" task to resolve the promise which will itself queue a microtask. This task can have a different priority than the timer one. For instance if they had a click event listener, the task from that click event handler would have had higher priority than the timer, and any microtask queued in that event handler would also be executed before. Basically, the order of 2 tasks from different sources is non-deterministic, the order of queued microtasks is.
I think I finally get the point you are making: as fetch will create a task when it gets the response (which in turn creates the promise job), there is no guarantee that the setTimeout callback -- that will be queued as a task later (in the asker's scenario) -- will end up in the same task queue, and so their mutual order is undefined.
|

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.