Masterbook/Chapter 5• 5 min read
View Raw Code on GitHub

What is Event Loop?

What is Event Loop?

The event loop is the coordination system that allows JavaScript to appear concurrent, even though it runs on a single thread. It manages what code runs now, what runs next, and when the browser can repaint the screen. Every JavaScript environment, like a browser or Node.js, includes an event loop, though their specific queues and priorities may differ slightly.

When JavaScript first starts running a script, it executes all synchronous code line by line on the call stack. This is the main thread of execution. When asynchronous operations like timers, fetch calls, or user interactions occur, they register callbacks to be executed later, once their operation finishes. Those callbacks are stored in different types of queues.

The event loop constantly checks two key conditions:

  1. Is the call stack empty?
  2. Are there any tasks waiting in the queues?

If the stack is empty, the event loop picks the next appropriate task and pushes it onto the stack to execute.

There are two main kinds of tasks the event loop manages — macrotasks and microtasks — and understanding their order is crucial.

Macrotasks (often just called tasks) include things like setTimeout, setInterval, setImmediate (in Node.js), I/O callbacks, and events such as clicks or network responses. Microtasks include promise .then() callbacks, async/await continuations, and functions queued using queueMicrotask. Microtasks always run before the event loop moves to the next macrotask.

Here's the exact sequence:

The event loop takes one macrotask from the macrotask queue (for example, the initial script or a setTimeout callback) and executes it fully, top to bottom.

When that macrotask completes, the event loop checks the microtask queue.

All microtasks in the queue are executed, one after another, until the microtask queue is completely empty.

Once the microtasks finish, the browser gets a chance to perform rendering or painting if needed.

Then, the loop picks the next macrotask and repeats the same steps.

Because microtasks are processed right after each macrotask and before rendering, promise .then() callbacks can run sooner than you might expect. For example, if you schedule both a promise and a setTimeout with zero delay, the promise handler will always run first because it is a microtask, and microtasks are drained before the next macrotask starts.

Consider this example:

console.log("Start");
setTimeout(() => console.log("Timeout callback"), 0);
Promise.resolve().then(() => console.log("Promise microtask"));
console.log("End");

/*
The output order is:
Start
End
Promise microtask
Timeout callback
*/

This happens because the main script itself is a macrotask. After it finishes, the event loop looks at the microtask queue. The promise .then() is there, so it runs before the next macrotask (the setTimeout callback).

If you add more microtasks while executing microtasks, those newly added ones are also run before the event loop returns to macrotasks. This is why microtasks can "chain" indefinitely if they keep scheduling more microtasks.

Another important detail is rendering. The browser does not repaint the screen in the middle of microtasks. Rendering occurs only after the microtask queue is empty and before starting the next macrotask. That's why promise-heavy operations can block the visual update even though they seem asynchronous.

In Node.js, the terminology differs slightly, but the logic is similar. Node has phases like timers, pending callbacks, I/O polling, and a microtask queue that behaves like the browser's.

To summarize the correct order clearly:

  • Run one macrotask (e.g., a piece of code, setTimeout callback, I/O event).
  • When that macrotask finishes, run all queued microtasks.
  • If more microtasks appear during this step, keep running them until none remain.
  • Let the browser render updates.
  • Move to the next macrotask.
  • Repeat indefinitely.
  • This precise sequence ensures that JavaScript remains non-blocking, responsive, and predictable despite running in a single thread.

Examples

console.log("A");
setTimeout(() => console.log("B"), 0);
Promise.resolve().then(() => console.log("C"));
console.log("D");
// Order: A, D, C, B
// microtasks drain before the next macrotask
setTimeout(() => console.log("timeout"), 0);
Promise.resolve().then(() => console.log("then-1"));
Promise.resolve().then(() => console.log("then-2"));
// then-1, then-2, timeout
// queueMicrotask behaves like a promise microtask
queueMicrotask(() => console.log("microtask"));
console.log("sync");
// sync, microtask

Common misconceptions

  1. setTimeout with zero delay runs immediately. It schedules a task that runs only after the current call stack is empty and microtasks have run.
  2. Promises are faster by themselves. It is not about speed but about queue priority. Promise callbacks run in the microtask queue which is processed before the next task.
  3. The event loop belongs to JavaScript the language. The event loop is provided by the host environment (browsers, Node.js) which integrates timers, I/O, and rendering.

Practice questions

  1. Predict the output order of logs when both setTimeout and Promise.then are used together.
console.log("1. Sync start");
setTimeout(() => {
  console.log("2. setTimeout");
}, 0);
Promise.resolve().then(() => {
  console.log("3. Promise");
});
console.log("4. Sync end");

/*
OUTPUT:
1. Sync start
4. Sync End
3. Promise
2. setTimeout
*/
  1. Explain why a long running while loop blocks click handlers from running.
  • A long-running while loop blocks click handlers from running because of JavaScript's single-threaded nature and how its Event Loop manages tasks.

  • In JavaScript, there is only one main thread executing your code. If that thread is busy doing heavy synchronous work, it physically cannot do anything else including updating the UI or listening to user interactions—until that work is finished.

  1. Show how to yield back to the event loop to keep the UI responsive during heavy computation.