12

In the following code:

setTimeout(() => console.log("hello"), 0);

Promise.resolve('Success!')
  .then(console.log)

What should happen in my understanding:

  1. setTimeout is called => print hello directly added to callback queue as time is 0
  2. Promise.resolve => print Success! added to callback queue

If I am not wrong, the callback queue is FIFO.

But the code output is:

Success!
hello

What is the explanation?

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Logan Wlv
  • 3,274
  • 5
  • 32
  • 54
  • Those tasks are going to different queues. A timer won't fire before the current script is finished. – Teemu Feb 26 '21 at 13:59
  • 1
    There are two different queues, the "task queue" and the "microtask queue". – Pointy Feb 26 '21 at 14:00
  • setTimeout function scheduled to run later. Please look at this similar question https://stackoverflow.com/questions/36904773/why-doesnt-settimeout-0-execute-immediately – maziyank Feb 26 '21 at 14:02
  • @maziyank this is different, both are lines are async code. – Logan Wlv Feb 26 '21 at 14:04
  • @Pointy oh ok didn't know that, so if I understand correctly we first execute non async code, then microtask queue then task queue ? – Logan Wlv Feb 26 '21 at 14:04
  • there is nothing in concern of queue or FIFO, this just process priorities and js engine implementations – Mister Jojo Feb 26 '21 at 14:14

3 Answers3

13

There are 2 separate queues for handling of the callbacks. A macro and a micro queue. setTimeout enqueues an item in the macro queue, while promise resolution - to the micro queue. The currently executing macro task (the main script itself in this case) is executed synchronously, line by line until it is finished. The moment it is finished, the loop executes everything queued in the microtask queue before continuing with the next item from the macro queue (which in your case is the console.log("hello") queued from the setTimeout).

Basically, the flow looks like this:

  1. Script starts executing.

MacrotaskQueue: [], MicrotaskQueue: [].

  1. setTimeout(() => console.log("hello"), 0); is encountered which leads to pushing a new item in the macrotask queue.

MacrotaskQueue: [console.log("hello")], MicrotaskQueue: [].

  1. Promise.resolve('Success!').then(console.log) is read. Promise resolves to Success! immediately and console.log callback gets enqueued to the microtask queue.

MacrotaskQueue: [console.log("hello")], MicrotaskQueue: [console.log('Success!')].

  1. The script finishes executing so it checks if there is something in the microtask queue before proceeding with the next task from the macro queue.
  2. console.log('Success!') is pulled from the microtask queue and executed.

MacrotaskQueue: [console.log("hello")], MicrotaskQueue: [].

  1. Script checks again if there is something else in the microtask queue. There is none, so it fetches the first available task from the macrotask queue and executes it, namely - console.log("hello").

MacrotaskQueue: [], MicrotaskQueue: [].

  1. After the script finishes executing the console.log("hello"), it once again checks if there is anything in the microtask queue. It is empty, so it checks the macrotask queue. It is empty as well so everything queued is executed and the script finishes.

This is a simplified explanation, though, as it can get trickier. The microtask queue normally handles mainly promise callbacks, but you can enqueue code on it yourself. The newly added items in the microtask queue will still be executed before the next macrotask item. Also, microtasks can enqueue other microtasks, which can lead to an endless loop of processing microtasks.

Some useful reference resources:

zhulien
  • 5,145
  • 3
  • 22
  • 36
10

There are two different queues involved here: a Task queue and a Microtask queue.

Callback functions scheduled using setTimeout are added in the task queue whereas the callbacks scheduled using promises are added in the microtask queue or a job queue.

A microtask queue is processed:

  • after each callback as long as the call-stack is empty.
  • after each task.

Also note that if a microtask in a microtask queue queues another microtask, that will also be processed before processing anything in the task queue. In other words, microtask queue will be processed until its empty before processing the next task in the task queue.

The following code snippet shows an example:

setTimeout(() => console.log('hello'), 0);

Promise.resolve('first microtask')
  .then(res => {
    console.log(res);
    return 'second microtask';
  })
  .then(console.log);

In your code, callback function of setTimeout is added to the task queue and the Promise.resolve queues a micro-task in a microtask queue. This queue is processed at the end of the script execution. That is why "success" is logged before "hello".

The following image shows a step-by-step execution of your code:

Enter image description here

Resources for further reading:

Yousaf
  • 27,861
  • 6
  • 44
  • 69
  • You have links to sources? I'd like to read more about it – Tony Feb 26 '21 at 14:08
  • 1
    @Tony everything is explained in detail here: https://javascript.info/event-loop – etarhan Feb 26 '21 at 14:09
  • You left out [articles](https://www.youtube.com/watch?v=1Dax90QyXgI&t=17m54s) left and right. Is there only *one* microtask queue (*the* microtask queue) or are several possible? – Peter Mortensen Feb 26 '21 at 23:23
  • @PeterMortensen i try to minimize grammatical errors when writing english but i am not a native english speaker. I appreciate you editing the answer to remove the grammatical mistakes. As far as your question regarding the microtask queue is concerned, in nodejs, there are two types of microtask queues, one holds the microtasks scheduled using `process.nextTick` and the other holds the microtasks scheduled using promises. – Yousaf Feb 27 '21 at 11:47
0

Even though the timeout is 0, the callback function will still be added to the web API (after being fetched from the call stack). Web APIs are threads that you can’t access; you can just make calls like Ajax, Timeout, and the DOM.

Promise.resolve schedules a microtask whereas setTimeout schedules a macrotask. Microtasks are executed before running the next macrotask.

So in your example, the

Promise.resolve('Success!').then(console.log);

will be executed before the setTimout since promises have better priority than the setTimeout callback function in the event loop stack.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Ran Turner
  • 14,906
  • 5
  • 47
  • 53
  • This is not quite true. Promise resolution happens in a different call stack than the currently-running macro task, so it's not executed "immediately". – Pointy Feb 26 '21 at 14:16
  • I updated the answer so it will be clearer – Ran Turner Feb 26 '21 at 14:21
  • 1
    "*Web APIs are threads that you can’t access*" - that doesn't sound right. It might be intended as a gross simplification, but I don't think that helps. – Bergi Feb 27 '21 at 16:38