Event Loop & Async Magic – How JavaScript Really Runs Your Code

Emma GeorgeEmma George
15 Aug, 2025
Event Loop & Async Magic – How JavaScript Really Runs Your Code

TABLE OF CONTENTS

Introduction

1 . The JavaScript Runtime Environment

2 . Single-Threaded Execution

3 . The Call Stack

4 . Web APIs (in Browsers)

5 . The Event Loop’s Core Job

6 . Macrotasks vs Microtasks

7 . Microtask Queue Priority

8 . setTimeout Gotchas

9 . Promises and the Event Loop

10 . Async/Await Behind the Scenes

11 . Long Tasks Blocking the Event Loop

12 . setInterval and the Event Loop

13 . requestAnimationFrame Timing

14 . Node.js Event Loop Phases

15 . Practical Debugging of Event Loop Issues

16 . Combining Microtasks & Macrotasks for Control

17 . Visualizing the Event Loop

Conclusion

Introduction

JavaScript is often called “single-threaded” and “non-blocking” in the same breath. Wait… how can it be both? How can a single-threaded language handle async tasks like network calls, timers, and UI rendering without freezing?

The answer lies in the Event Loop, a deceptively simple mechanism that gives JavaScript its asynchronous magic.

If you truly understand the event loop, you’ll unlock:

  • Predictable async behavior
  • Avoiding “callback hell”
  • Debugging weird timing bugs
  • Writing more performant code

In this guide, we’ll break it down step-by-step.

1 . The JavaScript Runtime Environment

Definition: The runtime is the environment where JavaScript executes, typically a browser or Node.js.

Description: It consists of:

  • Call Stack (where code runs)
  • Heap (where memory is stored)
  • Web APIs (timers, DOM, fetch, etc. in browsers)
  • Callback/Task Queue
  • Event Loop

Code Example:

console.log("Hello from the runtime!");

This runs in the call stack immediately.

2 . Single-Threaded Execution

Definition: JavaScript executes code on one main thread, one line at a time.

Description: Only one piece of code runs at any given moment. There’s no parallel execution (except in Web Workers).

Code Example:

console.log("Step 1");
console.log("Step 2");
console.log("Step 3");

Always prints in order.

3 . The Call Stack

Definition: A stack data structure that tracks which functions are currently running.

Description: Functions are pushed when called, and popped when done.

Code Example:

function a() { b(); }
function b() { c(); }
function c() { console.log("Inside C"); }
a();

Execution order: a → b → c.

4 . Web APIs (in Browsers)

Definition: Browser-provided features for async operations: setTimeout, DOM events, fetch, etc.

Description: These run outside the call stack and hand results back later.

Code Example:

console.log("Start");
setTimeout(() => console.log("Timer done"), 1000);
console.log("End");

Output:

Start
End
Timer done

5 . The Event Loop’s Core Job

Definition: The Event Loop checks if the call stack is empty, then pushes queued callbacks onto it.

Description: It’s like a traffic cop, making sure tasks don’t block each other.

Code Example:

console.log("Script start");

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

console.log("Script end");

Output order proves async nature.

6 . Macrotasks vs Microtasks

Definition: Macrotasks: setTimeout, setInterval, setImmediate

Microtasks: Promise callbacks, queueMicrotask, MutationObserver

Description: Microtasks run before the next rendering or macrotask.

Code Example:

setTimeout(() => console.log("Macrotask"), 0);
Promise.resolve().then(() => console.log("Microtask"));
console.log("Script");

Output:

Script
Microtask
Macrotask

7 . Microtask Queue Priority

Definition: Microtasks are processed immediately after the current task, before rendering.

Description: Promises often run before timers.

Code Example:

setTimeout(() => console.log("Timer"), 0);
queueMicrotask(() => console.log("Queue Microtask"));
console.log("Main");

Output:

Main
Queue Microtask
Timer

8 . setTimeout Gotchas

Definition: setTimeout(fn, 0) means minimum delay, not “instant”.

Description: It’s scheduled after current call stack and microtasks finish.

Code Example:

setTimeout(() => console.log("Timeout"), 0);
Promise.resolve().then(() => console.log("Promise"));
console.log("Sync");

Output:

Sync
Promise
Timeout

9 . Promises and the Event Loop

Definition: Promises resolve into microtasks.

Description: They run faster than timers but still after synchronous code.

Code Example:

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

Output:

Sync
Promise 1
Promise 2

10 . Async/Await Behind the Scenes

Definition: async/await is syntactic sugar for Promises.

Description: await pauses the function until the Promise resolves, resuming as a microtask.

Code Example:

async function run() {
  console.log("Before await");
  await null;
  console.log("After await");
}
run();
console.log("Outside");

Output:

Before await
Outside
After await

11 . Long Tasks Blocking the Event Loop

Definition: A long-running function can freeze the UI because the event loop can’t process other tasks.

Code Example:

console.log("Start");
for (let i = 0; i < 1e9; i++) {} // Block
console.log("End");

No other code runs until this finishes.

12 . setInterval and the Event Loop

Definition: setInterval schedules repeated tasks but can be delayed if the loop is busy.

Code Example:

setInterval(() => console.log("Tick"), 1000);

If you block the loop, ticks are delayed.

13 . requestAnimationFrame Timing

Definition: Browser API for smooth animations, synced with refresh rate.

Description: Runs before the next repaint.

Code Example:

function draw() {
  console.log("Drawing");
  requestAnimationFrame(draw);
}
draw();

14 . Node.js Event Loop Phases

Definition: Node’s loop has phases: timers, I/O callbacks, idle, poll, check, close callbacks.

Description: setImmediate vs setTimeout behave differently.

Code Example (Node.js only):

setImmediate(() => console.log("Immediate"));
setTimeout(() => console.log("Timeout"), 0);
console.log("Main");

15 . Practical Debugging of Event Loop Issues

Definition: Understanding the loop helps fix async race conditions.

Code Example:

let data;
fetch("/api").then(res => res.json()).then(json => {
  data = json;
});
console.log(data); // undefined (fetch not done yet)

Solution: Work inside the async callback or use await.

16 . Combining Microtasks & Macrotasks for Control

Description: Sometimes you need to control execution order.

Code Example:

Promise.resolve().then(() => console.log("Microtask 1"));
setTimeout(() => console.log("Macrotask 1"), 0);
Promise.resolve().then(() => console.log("Microtask 2"));

17 . Visualizing the Event Loop

Description: It’s helpful to think:

  • Run all sync code
  • Empty microtask queue
  • Process next macrotask
  • Repeat

Conclusion

The Event Loop isn’t just a quirky detail, it’s the heartbeat of JavaScript. Understanding it means you can:

  • Predict async behavior
  • Write smoother UIs
  • Avoid subtle bugs
  • Optimize performance

From call stack to microtasks and macrotasks, mastering the event loop gives you true control over how and when your code runs.

Emma George

Emma George

Software Engineer

Senior Software Engineer