← Back to Chapters

JavaScript Event Loop

? JavaScript Event Loop

⚡ Quick Overview

The JavaScript event loop is the mechanism that lets JavaScript handle asynchronous, non-blocking operations even though the language itself runs on a single thread. It continuously coordinates:

  • Execution of synchronous code on the call stack.
  • Delegation of async work to Web APIs (in browsers) or the Node.js runtime.
  • Processing of callbacks stored in queues and microtask queues.

? Key Concepts

  • Call Stack: Where JavaScript runs synchronous code, function by function.
  • Web APIs: Browser features (timers, DOM events, fetch, etc.) that handle async work.
  • Callback Queue (Task Queue): Stores callbacks for macrotasks like setTimeout.
  • Microtask Queue: Stores higher-priority tasks like Promise.then callbacks.
  • Event Loop: Continuously checks the call stack and queues and moves tasks into the stack when it’s free.

In short, the event loop:

  1. Runs all synchronous code first.
  2. Handles microtasks (Promises) right after the current stack frame.
  3. Then processes macrotasks (timers, I/O) from the callback queue.

? Syntax and Theory

JavaScript does not expose the event loop directly via syntax, but you interact with it using:

  • setTimeout, setInterval → schedule macrotasks.
  • Promise.then, async/await → schedule microtasks.
  • Node.js adds additional phases (timers, I/O callbacks, poll, check, close callbacks) using the libuv library.

A typical flow looks like this:

Call Stack Web APIs Callback Queue Event Loop

? Code Examples

? Example – Basic Event Loop with setTimeout

This example shows how a timer callback is delayed until after synchronous code finishes:

? View Code Example: Basic Event Loop
console.log("Start");

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

console.log("End");

// Expected output:
// Start
// End
// Timeout callback

? Example – Macrotasks vs Microtasks

Here we compare setTimeout (macrotask) with a resolved Promise (microtask):

? View Code Example: Macro and Microtasks
console.log("Script start");

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

Promise.resolve().then(() => console.log("Promise then"));

console.log("Script end");

// Expected output:
// Script start
// Script end
// Promise then
// setTimeout

? Live Output and Explanation

?️ Example Console Output – Basic Event Loop

Start
End
Timeout callback

Even though setTimeout(..., 0) uses a delay of 0 milliseconds, its callback is placed in the callback queue and will only be executed after:

  • All current synchronous code (console.log("Start"), console.log("End")) finishes.
  • The call stack becomes empty.

? Example Console Output – Macro vs Microtasks

Script start
Script end
Promise then
setTimeout

The microtask (Promise.then) runs before the macrotask (setTimeout), which is why Promise then appears before setTimeout in the output.

? Browser vs Node.js Event Loop

Both browsers and Node.js use the event loop idea but their implementations differ slightly:

  • Browser: Focuses on tasks (macrotasks), microtasks, and rendering. Web APIs handle timers, DOM events, network requests, etc.
  • Node.js: Uses the libuv library and processes multiple phases: timers, I/O callbacks, idle, poll, check, and close callbacks. Microtasks (Promises, process.nextTick) are also woven into these phases.

?️ Interactive Example

Click the button to see the execution order produced by the event loop in real time:

? Log

Click the button to generate logs.

? Use Cases

  • Handling user interactions (clicks, keypresses) without freezing the UI.
  • Running network requests (AJAX/fetch) without blocking other code.
  • Animating elements using requestAnimationFrame while keeping the page responsive.
  • Building highly concurrent servers in Node.js that handle many connections efficiently.

? Tips and Best Practices

  • Prefer Promises or async/await for clearer and more predictable async flows.
  • Always remember: microtasks run before the next macrotask.
  • Keep synchronous code light; heavy loops or CPU work block the event loop and freeze the UI.
  • Break long-running tasks into smaller chunks using timers or requestIdleCallback where possible.
  • Do not assume setTimeout(fn, 0) runs immediately; it waits for the current stack and microtasks to complete.
  • Be careful with recursion or nested callbacks; they can fill the stack and delay queued tasks.

? Try It Yourself

  • Write a small script using setTimeout and Promise.then and predict the output order before running it in the console.
  • Create a blocking while loop that runs for a few seconds and observe how timers and UI interactions are delayed.
  • Implement an async function that awaits a Promise and log the order of messages around it.
  • In Node.js, experiment with process.nextTick and setImmediate and compare when their callbacks run.