The Event Loop: JavaScript's Secret Traffic Controller

2025-04-04

You’ve probably heard someone say “JavaScript is single-threaded” and nodded along while quietly wondering what that actually means and why anyone should care.

Fair. Let’s fix that today.

The event loop is the reason JavaScript can feel fast and responsive even though it only does one thing at a time. It’s the secret behind every setTimeout, every fetch(), every smooth animation. Once you get it, a huge chunk of async confusion just… dissolves.

Let’s walk through it together.


First: What Even Is “Single-Threaded”?

Imagine a restaurant with only one cook.

Every order that comes in has to wait for the cook to finish the current one. No parallel cooking. One task at a time, in sequence.

That’s JavaScript. One thread, one call stack, one thing happening at any given moment.

But here’s the twist — that one cook is incredibly organized. They have a system for handling tasks that take a long time (like waiting for water to boil) without standing there doing nothing. They hand off the slow stuff and come back to it later.

That system is the event loop.


The Main Players

To understand the event loop, you need to meet the crew:

🧠 The Call Stack

This is where your code actually runs. When you call a function, it gets pushed onto the stack. When it finishes, it gets popped off.

function greet() {
  console.log("Hello!");
}

greet(); // pushed → runs → popped

Simple. Linear. One thing at a time.

🌐 Web APIs (a.k.a. The Kitchen Staff)

When you call something like setTimeout, fetch, or an event listener — JavaScript doesn’t handle that itself. It hands it off to the browser’s Web APIs (or Node’s built-ins, if you’re server-side).

These run outside of your JavaScript thread. They’re doing their thing in the background while your code keeps going.

console.log("1");

setTimeout(() => {
  console.log("2");
}, 1000);

console.log("3");

// Output:
// 1
// 3
// 2  ← shows up 1 second later

That delay? The Web API handled it. JavaScript just kept going.

📬 The Callback Queue (Task Queue)

When a Web API finishes (your timer fires, your fetch returns, your click is clicked), it doesn’t immediately run your callback. Instead, it drops the callback into the callback queue — a waiting room.

🔄 The Event Loop Itself

Here’s the loop (literally):

“Is the call stack empty? Cool — grab the next thing from the queue and push it on.”

That’s it. The event loop just watches. When the call stack clears, it pulls from the queue and runs the next task.

Call Stack → empties → Event Loop → grabs from Queue → pushes callback → runs → repeat

A Real Walkthrough

Let’s trace through this code step by step:

console.log("start");

setTimeout(() => {
  console.log("timeout!");
}, 0);

console.log("end");

You might expect timeout! to appear between start and end since the delay is 0ms. But the output is:

start
end
timeout!

Why?

  1. console.log("start") → runs immediately, stack clears.
  2. setTimeout(...) → handed off to the Web API. Even with 0ms, it’s async.
  3. console.log("end") → runs immediately, stack clears.
  4. Now the stack is empty. The event loop picks up the callback from the queue.
  5. console.log("timeout!") → finally runs.

The 0ms delay doesn’t mean “run instantly” — it means “run as soon as the stack is clear.”


Enter the Microtask Queue

Here’s where it gets spicy.

There’s actually two queues. The callback queue you already met. But there’s also a microtask queue — and it has priority.

Microtasks come from:

The rule is:

After each task, drain the entire microtask queue before moving to the next task.

So microtasks always jump the line.

console.log("1");

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

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

console.log("4");

// Output:
// 1
// 4
// 3  ← microtask runs before setTimeout!
// 2

Walk through it:

  1. "1" → runs
  2. setTimeout → queued as a macrotask
  3. Promise.resolve().then(...) → queued as a microtask
  4. "4" → runs
  5. Stack is empty → drain microtask queue → "3" runs
  6. Stack is empty again → grab next macrotask → "2" runs

This is why Promise-based code tends to feel more “immediate” than setTimeout.


Why Does This Actually Matter?

Great question. Here’s where it stops being theory and starts being practical.

1. Debugging Async Code

Ever stared at async code and thought “why is this running out of order?” — now you know. The event loop isn’t random. It’s totally deterministic. You can trace exactly why something runs when it does.

2. Performance

If you block the call stack — say, with a massive for loop or a synchronous JSON.parse on a huge file — the event loop can’t grab anything from the queue. Your UI freezes. Your clicks don’t register. Your users are sad.

// ⚠️ This will freeze the browser for a moment:
for (let i = 0; i < 1_000_000_000; i++) {}

Keep the main thread clear. Break up long tasks. Use workers for heavy lifting.

3. Race Conditions and Ordering

When you have multiple async operations, the event loop decides the order. Knowing how microtasks vs. macrotasks queue up helps you reason about which promise resolves first, why a DOM update happens before or after your callback fires, and so on.


A Mental Model to Keep

Think of it this way:

Your Code (synchronous)
    ↓ runs completely
Web APIs (async work)
    ↓ finishes, drops callback
Microtask Queue (promises)
    ↓ drains completely after each task
Callback Queue (timers, events)
    ↓ one task at a time
Event Loop
    ↓ orchestrates all of it

The event loop is the conductor. It never rushes. It never skips. It just keeps asking: “Is the stack empty? What’s next?”


The TL;DR


Where to Go Next

Once the event loop clicks, a whole new layer of JavaScript opens up:

But honestly? Just knowing what you know now puts you ahead of a lot of developers who’ve been writing JavaScript for years.

The event loop is one of those concepts that rewards the time you spend on it. Not just for interviews — but for those moments at 2am when your async code isn’t doing what you expect, and suddenly it all makes sense.

Happy looping. 🔄