DEV Community

Cover image for The JavaScript Event Loop, Visualized
Chioma Halim
Chioma Halim

Posted on • Originally published at blog.audreyhal.com

The JavaScript Event Loop, Visualized

This is a cross-post of the original article.
Read the full version, including the interactive visualizations, on audreyhal.com.

Quick quiz. What does this print, and in what order?

console.log("start");

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

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

console.log("end");
Enter fullscreen mode Exit fullscreen mode

The answer is:

start
end
promise1
promise2
timeout
Enter fullscreen mode Exit fullscreen mode

If that's not what you guessed, don't worry. Most people don't get it right the first time.

By the end of this post, you'll know exactly why it happens.

The Four Pieces, at a Glance

"The event loop" gets used as a catch-all term. But it's really just one part of a small system with four pieces:

  • The call stack: where your code runs, one step at a time
  • Web APIs: browser tools (like timers and network requests) that can do work outside your code
  • Two queues: waiting lines for work that's finished and ready to run
  • The event loop: the thing that moves work from those queues onto the call stack

Each piece is simple on its own. Let's go through them one at a time, starting with the call stack.

The Call Stack

JavaScript can only do one thing at a time. The call stack is how it keeps track of that one thing.

The original article includes an interactive call stack visualization.

Here's how it works:

  • When a function is called, it gets added to the top of the stack.
  • When that function finishes, it's removed.
  • If a function calls another function, the new one goes on top. It has to finish first.

So the stack just grows and shrinks as your code runs. But it always works from the top down. A new task can't start until whatever's on top is done.

Why That's a Problem

Say something on the stack takes a long time. A big loop, maybe, or a slow calculation.

While that's running, nothing else can happen. No clicks. No other code. No updates on screen. The page just waits.

That's fine for something quick. But it's a real problem for something like a network request, which can take seconds.

If fetch worked this way, sitting on the stack until it finished, every page making a request would freeze. So JavaScript needs another way to handle work like this.

That's what Web APIs are for.

Web APIs: Handing Off the Work

Web APIs are tools the browser gives you. Timers, the DOM, network requests, and more. They live outside JavaScript itself.

When you call one, you're not doing the work yourself. You're asking the browser to do it for you.

Here's what that looks like with setTimeout:

The original article includes an interactive Web API visualization.

Calling setTimeout briefly touches the call stack. But all it does is register the callback and the delay. Then it's removed right away.

It doesn't wait around. The browser handles the countdown on its own, and your code moves straight to the next line.

That handoff is the key idea. Starting the async work is quick. The actual waiting happens somewhere else.

The Task Queues

Once a piece of Web API work is done, its callback doesn't go straight back onto the call stack. That could interrupt whatever's already running.

Instead, it waits in line.

There are two separate lines, and which one a callback joins depends on what kind of work it came from.

The Macrotask Queue

Callback-based work, like our setTimeout timer, goes into the macrotask queue.

You'll also hear this called the callback queue or task queue.

The original article includes an interactive task queue visualization.

This is why setTimeout(fn, 0) doesn't mean "run this now."

It means "start a timer, and once it's done, get in line."

Getting in line isn't the same as running right away. If the call stack is busy when the timer finishes, the callback just waits, no matter how long that takes.

The delay you set is a minimum wait, not a guarantee.

The Microtask Queue

Promises, like fetch, work the same basic way, but they wait in a different line: the microtask queue.

The original article includes an interactive microtask visualization.

This queue is only for a few specific things: .then(), .catch(), .finally(), and code that runs after await.

When you call fetch, JavaScript creates a pending promise right away and hands the request off to the browser, just like it did with the timer. Your code keeps going.

When the response comes back later, the .then() callback doesn't run right away either. It gets added to the microtask queue and waits its turn.

One thing to watch for: a microtask can schedule another microtask. If that keeps happening, the event loop can get stuck working through that queue and never reach the macrotask queue.

This is rare, but it's worth knowing about, especially if you ever see a timer that seems to never fire.

The Event Loop

Now there are two queues full of callbacks, waiting their turn.

Something has to move them onto the call stack. That's the event loop's whole job.

It does one simple thing, over and over: check if the call stack is empty, and if it is, take the next thing waiting in a queue and run it.

There's one rule that matters most:

The event loop always finishes the entire microtask queue before it looks at the macrotask queue.

Every microtask goes first, even new ones added while it's still working through the queue.

This is the part that explains our quiz.

Back to the Quiz

Now we have everything we need to walk through it, step by step.

The original article includes an interactive event loop visualization.

  1. console.log("start") runs right away → start prints.
  2. setTimeout(...) hands its callback to the browser. The script keeps going.
  3. Promise.resolve().then(...) is already resolved, so its callback goes straight into the microtask queue.
  4. The second .then(...) gets added right behind it.
  5. console.log("end") runs → end prints.
  6. The script is done. The call stack is empty.
  7. The event loop checks the microtask queue first. Both callbacks run: promise1, then promise2.
  8. Only now does it check the macrotask queue. The timer's callback is ready → timeout prints.
start
end
promise1
promise2
timeout
Enter fullscreen mode Exit fullscreen mode

It was scheduled first and ran last.

Not a bug, just the queues doing what they're supposed to.

The Short Version

  • JavaScript does one thing at a time, tracked by the call stack.
  • Long-running work would freeze the stack, so it gets handed off instead.
  • Web APIs do that work in the background, then return the result through a queue.
  • Timers go in the macrotask queue. Promises go in the microtask queue.
  • The event loop always clears the microtask queue before moving to the macrotask queue.

Once that clicks, a lot of "why did this run in this order" confusion goes away.


This is the first post in a series on JS and React fundamentals. Next up, the render cycle: what actually happens between calling setState and pixels changing on screen.

Top comments (0)