JavaScript async callbacks - Promise and setTimeout

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:

  • Tasks, microtasks, queues and schedules

  • JavaScript job queue and microtasks


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:

  • The event loop
  • Microtasks
  • Using Microtasks