100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
JavaScript

The Event Loop in JavaScript

Understand the call stack, macrotask queue, and microtask queue, and master the ordering rules that govern async execution.

Asynchronous JavaScriptAdvanced12 min readJul 8, 2026
Analogies

1. Introduction

The event loop is the mechanism that allows JavaScript -- a single-threaded language with one call stack -- to handle asynchronous operations like timers, network requests, and Promise callbacks without blocking. Understanding it precisely is essential for correctly predicting the order in which asynchronous code executes, which is one of the trickiest and most commonly misunderstood areas of JavaScript.

🏏

Cricket analogy: A bowler (call stack) can only bowl one delivery at a time, but the ground staff (Web APIs) track a DRS review in the background so the umpire doesn't have to stall the game waiting for it.

The runtime consists of a few key pieces: the call stack (where currently executing function calls live), Web APIs / Node APIs (which handle timers, I/O, and network operations outside the main thread), and two kinds of queues that feed completed work back into the call stack -- the macrotask queue (also called the task or callback queue) and the microtask queue.

🏏

Cricket analogy: The playing XI on the field is the call stack, the dressing room's analysts running instant replays are the Web APIs, and two lists -- over-by-over milestones (macrotask) and quick no-ball rechecks (microtask) -- feed results back to the umpire.

2. Syntax

javascript
// Macrotask sources
setTimeout(() => {}, 0);
setInterval(() => {}, 1000);

// Microtask sources
Promise.resolve().then(() => {});
queueMicrotask(() => {});

// Simplified event loop algorithm:
// 1. Run the current synchronous script to completion.
// 2. Drain the ENTIRE microtask queue (including any new microtasks
//    queued while draining it) before doing anything else.
// 3. Take ONE macrotask from the queue and run it.
// 4. Drain the microtask queue again.
// 5. Repeat from step 3.

3. Explanation

The call stack executes synchronous code immediately, in order. When you call an asynchronous API like setTimeout or fetch, the actual work is delegated outside the call stack; once it's ready, its callback is placed into the appropriate queue rather than being run right away. The event loop's job is to move work from these queues back onto the call stack, but ONLY when the call stack is completely empty.

🏏

Cricket analogy: The umpire only signals the next ball once the current delivery, run, and appeal are fully resolved on the pitch -- a DRS request raised mid-delivery waits until the call stack (live action) is completely empty.

The crucial rule that trips up most developers is the priority between the two queues: after every single macrotask finishes (including the very first run of the main script), the JavaScript engine fully drains the ENTIRE microtask queue -- running every microtask, including any new ones that get queued by earlier microtasks -- before it is allowed to pick up the next macrotask. Promise .then/.catch/.finally callbacks and queueMicrotask() go into the microtask queue. setTimeout, setInterval, and I/O callbacks go into the macrotask queue.

🏏

Cricket analogy: After every over (macrotask) is bowled, the umpire must clear every pending DRS review (microtask) -- even new reviews raised during those reviews -- before the next over can start, just like promise callbacks always jumping ahead of the next setTimeout over.

This is why Promise.resolve().then(() => console.log('microtask')) always logs before setTimeout(() => console.log('macrotask'), 0), even though both were scheduled 'immediately' -- the entire microtask queue is emptied before the event loop is even allowed to look at the macrotask queue again, regardless of the requested setTimeout delay.

4. Example

javascript
console.log("1 - script start");

setTimeout(() => {
  console.log("2 - setTimeout callback");
}, 0);

Promise.resolve()
  .then(() => {
    console.log("3 - promise 1");
  })
  .then(() => {
    console.log("4 - promise 2");
  });

console.log("5 - script end");

5. Output

text
1 - script start
5 - script end
3 - promise 1
4 - promise 2
2 - setTimeout callback

6. Key Takeaways

  • The call stack runs synchronous code first, always, before any queued async callback.
  • The microtask queue (Promises, queueMicrotask) is fully drained before the engine ever looks at the macrotask queue.
  • The macrotask queue (setTimeout, setInterval, I/O, UI events) is processed one task at a time, with a full microtask drain after each one.
  • A setTimeout(fn, 0) callback ALWAYS runs after all currently pending microtasks, no matter how many there are.
  • New microtasks queued during microtask processing are still processed before the next macrotask -- microtasks can 'starve' macrotasks if they keep scheduling more of themselves.
  • Reliable async ordering requires knowing whether an API queues a microtask or a macrotask, not just its stated delay.

Practice what you learned

Was this page helpful?

Topics covered

#JavaScript#JavaScriptProgrammingStudyNotes#Programming#TheEventLoopInJavaScript#Event#Loop#Syntax#Explanation#Loops#StudyNotes#SkillVeris