I recently saw a basic question and realized my understanding of the JS event loop was not solid. So I organized it here to reinforce the fundamentals.
The basic question
Here is the original question:
console.log('start');
setTimeout(() => {
console.log('children2');
Promise.resolve().then(() => {
console.log('children3');
});
}, 0);
new Promise(function (resolve, reject) {
console.log('children4');
setTimeout(function () {
console.log('children5');
resolve('children6');
}, 0);
}).then((res) => {
console.log('children7');
setTimeout(() => {
console.log(res);
}, 0);
});
Correct answer
start
children4
children2
children3
children5
children7
children6
Explanation
- Macro task prints
start - setTimeout enters the EventTable and registers, timer starts
- Promise executor runs immediately, prints
children4, then setTimeout enters EventTable. First round ends - First timer enters queue, prints
children2, promise.then added to micro queue, printschildren3 - Second timer runs, prints
children5, promise.then added to micro queue, printschildren7 - Timer fires, added to macro task, prints
children6
Key concepts
JS is single-threaded. The event loop is JS’s execution mechanism, and other languages differ (for example, Java supports multithreading).
Synchronous and asynchronous tasks go into different execution contexts.

Async tasks can be divided into macro tasks and micro tasks.
Example: A bank clerk handling customers is a macro task. If a customer adds a small extra request, that can be considered a micro task.
Each macro task has a micro task queue. After each macro task finishes, the micro tasks are drained before the next macro task.

Macro tasks: setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, UI rendering
Micro tasks: process.nextTick (Node), Promises, queueMicrotask, MutationObserver
Even with 0 delay, setTimeout does not run immediately. Only after the call stack is empty will tasks be pulled from the Event Queue.
Promise represents the final completion (or failure) of an async operation and its result.
The event loop consists of the main thread and task queue. The main thread keeps pulling tasks from the queue and executing them.
Practice questions
Q1
NOTE: Run in Node.js.
console.log('1');
setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
process.nextTick(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})
setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})
Correct answer
1
7
6
8
2
4
3
5
9
11
10
12
Explanation
Macro task prints
1setTimeouts go into the delay queue
process.nextTick creates a micro task (not executed yet)
Promise executor runs immediately, prints
7, .then creates a micro taskAnother setTimeout goes into the delay queue
No more sync code, run micro tasks:
6,8Task stack cleared, timers are ready, move to message queue and execute
Print
2,4Run micro tasks:
3,5Print
9,11Run micro tasks:
10,12
Q2
console.log('script start');
setTimeout(function () {
console.log('setTimeout');
}, 0);
Promise.resolve()
.then(function () {
console.log('promise1');
})
.then(function () {
console.log('promise2');
});
console.log('script end');
Correct answer
script start
script end
promise1
promise2
setTimeout
Explanation
- Macro task prints
script start - Timer goes into event table
- promise.then is a micro task, not executed until macro task ends
- Macro task prints
script end - Macro task ends, run micro tasks:
promise1,promise2 - Micro tasks end, timer fires, go to event queue, execute and print
setTimeout
Q3
function foo() {
setTimeout(foo, 0);
}
foo();
function foo() {
return Promise.resolve().then(foo);
}
foo();
Snippet 1 will not overflow the stack, but snippet 2 will.
Explanation
- setTimeout is a macro task, Promise is a micro task. Macro tasks are pushed one per loop cycle, but micro tasks are always drained before returning to the event loop. If you keep adding micro tasks faster than you process them, the loop never returns to rendering and you will overflow.
Non-blocking?
Now that we understand the event loop and concurrency model, let’s mention non-blocking. When talking about Node.js, people often mention async non-blocking.
Note: async and non-blocking are two concepts. JS is single-threaded, so only one thing runs at a time. To improve responsiveness, async exists. Async means you can continue without waiting for results, so it is non-blocking.
What is blocking? For example, in the browser, if you call alert, nothing else can happen until the user clicks OK. That is blocking.
Note that async non-blocking is a JS feature. Browser JS also has it.
Final Thoughts
This post is rough. If there are issues, please point them out.

