DEV Community

Sahil Nasariwala
Sahil Nasariwala

Posted on Originally published at Medium AI-assisted

I Built an Event Loop Visualizer Because the Best One Is From 2014

Every JavaScript developer has seen this:

console.log('start')

setTimeout(() => console.log('timeout'), 0)

Promise.resolve().then(() => console.log('promise'))

console.log('end')

// start → end → promise → timeout
Enter fullscreen mode Exit fullscreen mode

Most of us know the answer.

We know that microtasks run before the next task. We know setTimeout(..., 0) doesn't actually mean "run this immediately." We know promises and the event loop are frequent interview topics.

But then someone gives you a slightly uglier example and asks:

"Okay, what happens next?"

And suddenly you're drawing boxes on a whiteboard.

That's what made me build The Event Loop, Visualized.

It's an interactive course where you can actually watch the call stack, task queue, microtask queue, and Web APIs change step by step. You can slow it down, move backward, scrub through the timeline, and eventually paste in your own JavaScript and watch it get visualized.

But don't we already have event loop visualizers?

Yes.

That's actually where this project started.

Before writing any code, I spent some time looking at what already existed.

The first one everyone knows is Loupe, from 2014. It's still great, and Philip Roberts' "What the heck is the event loop anyway?" talk is one of the resources that made the event loop click for an entire generation of developers.

But JavaScript has changed quite a bit since then.

Promises weren't part of the picture in the same way. async/await didn't exist. Microtasks weren't something most developers were thinking about.

There are newer tools too.

JSV9000 does a good job of showing microtasks and letting you step through execution, but it assumes you already understand what you're looking at.

JSFlow supports promises and async/await, but feels more like a playground than a course.

And then there's Lydia Hallie's JavaScript Visualized series, which is probably the best example of how much people enjoy learning JavaScript through animation. But they're still essentially videos/GIFs—you can't stop at a specific moment and ask, "Wait, why did this move here?"

I kept noticing the same thing:

The tools were either good at teaching or good at experimenting. Not both.

So I decided to try combining the two.

Not another diagram.

Not another article explaining the event loop.

Something you could actually play with.

The first thing I wanted to get right: different explanations, same animation

One thing I didn't want to do was create a "beginner version" and an "advanced version" as completely separate experiences.

Instead, every scene has three levels of explanation.

Explain simply

Think chef, kitchen, and VIP line.

It's deliberately visual and avoids throwing terms like "microtask checkpoint" at someone who has never heard them before.

Developer

Now we talk about the actual things developers work with:

  • Call stack
  • Web APIs
  • Task queue
  • Microtask queue
  • Promises
  • Timers

Under the hood

This is where the browser's actual behavior comes in: task sources, microtask checkpoints, rendering opportunities, and the HTML specification.

The interesting part is that the animation doesn't change.

Only the explanation changes.

That means a beginner and an experienced JavaScript developer can watch the exact same animation and just choose how deep they want to go.

It also turned out to be a useful constraint while building the content. I couldn't accidentally create three slightly different versions of the same lesson.

The architecture decision I liked the most

This is probably the most interesting part of the project from an engineering perspective.

My first thought was to instrument the browser.

Something like:

const originalSetTimeout = window.setTimeout

window.setTimeout = (...args) => {
  // record what happened
  return originalSetTimeout(...args)
}
Enter fullscreen mode Exit fullscreen mode

Intercept timers.

Watch promise callbacks.

Track the call stack.

Emit events as the browser actually executes them.

It sounds reasonable.

It's also a pain if you want an interactive visualizer.

Because now you're trying to control the actual event loop.

You can't simply say:

"Pause the browser right here."

And then:

"Actually, go back three steps."

Or:

"Let's replay this at 0.5× speed."

The browser isn't designed around those controls.

So I stopped trying to record the real event loop.

I decided to record what the event loop should do instead.

A scene is just a script

Instead of treating a scene as executable JavaScript, I represent it as a sequence of small actions.

Something roughly like:

Step 1
→ highlight line 1
→ push "console.log" onto stack
→ print "start"

Step 2
→ register setTimeout
→ move callback to Web APIs

Step 3
→ create promise callback
→ add it to microtask queue

...
Enter fullscreen mode Exit fullscreen mode

The visualizer doesn't care how the scene was created.

It just receives the steps and renders them.

The runtime state at any point can then be calculated from those steps:

export function computeState(
  scene: Scene,
  stepIndex: number
): RuntimeState {
  const state = emptyState()

  for (let i = 0; i <= stepIndex; i++) {
    for (const action of scene.steps[i].actions) {
      state.apply(action)
    }
  }

  return state
}
Enter fullscreen mode Exit fullscreen mode

And suddenly a lot of features became almost trivial.

Want to go backward?

Recalculate the state at the previous step.

Want to jump to step 7?

Calculate step 7.

Want to scrub from step 2 to step 9?

Calculate step 9 and animate the difference.

No undo stack.

No complicated event-sourcing system.

No snapshots.

Most scenes only have around ten or so steps, so recalculating the state is basically free.

That one architectural decision gave me:

  • Play
  • Pause
  • Step forward
  • Step backward
  • Scrubbing
  • Speed control
  • Deep links to specific steps

Then came the harder question: what about my own code?

Hand-written scenes work really well for lessons.

But eventually the obvious question appears:

"Cool. But can I paste my code into it?"

That was the whole point.

So the playground uses Acorn to parse the JavaScript into an AST and then compiles the supported code into the exact same step format used by the hand-written lessons.

The visualizer doesn't know—or care—whether a scene came from a lesson or from the playground.

It's just a sequence of steps.

For now, the playground supports a deliberately limited subset:

  • console.log
  • setTimeout
  • Promise chains
  • queueMicrotask
  • async/await
  • Function calls

There's also an 80-step limit.

I don't want someone pasting an accidental infinite loop into an educational animation and making the browser regret its life choices.

I also made a mistake here

I didn't build the compiler first.

I built the flagship scenes manually.

promise-vs-setTimeout

Microtask starvation

async/await, unmasked

After building enough of them, I started noticing the patterns.

Only then did I build the compiler.

That turned out to be a much better approach.

If I'd started by designing the compiler, I would have designed an abstraction based on what I thought a scene needed.

After building the scenes, I knew what it actually needed.

Some small things ended up making a big difference

The event loop is the main feature, but a few other decisions made the project feel much more complete.

Prediction mode

Before showing the answer, the site asks you to predict the output.

There's something useful about committing to an answer first.

If you're wrong, you don't just see the correct output—you see exactly where your mental model diverged.

"Break the loop"

There's a small sandbox where you can intentionally block the main thread.

You see an animation running normally.

Then the blocking loop starts.

And the animation freezes.

It's much more memorable than reading:

"Long-running JavaScript can block rendering."

You actually watch the UI stop responding.

Compare snippets

You can run two to four snippets against each other and watch their behavior side by side.

The benchmarks run inside a Web Worker so the comparison doesn't block the very event loop you're trying to understand.

Themes

There are five themes inspired by tools like Monkeytype.

The theme system uses CSS variables, and an inline pre-paint script prevents the page from flashing the wrong theme during startup.

It's a tiny detail.

But once you notice a flash like that, you can't unsee it.

Deep links

You can link directly to a specific point in an animation:

/play/promise-vs-settimeout?step=5&level=simply
Enter fullscreen mode Exit fullscreen mode

So instead of telling someone:

"Go to the promise vs setTimeout example and move to step 5."

You can just send them the exact moment.

I also added keyboard controls and reduced-motion support along the way.

What I'd like to build next

The obvious next step is Node.js's event loop.

Browser JavaScript and Node.js share the same language, but their event loop behavior has important differences.

Phases.

process.nextTick.

setImmediate.

Timers.

I want to put the browser and Node models next to each other and let people experiment with both.

And because the visualizer is based on step scripts rather than browser instrumentation, the underlying architecture doesn't really care which event loop we're describing.

That's probably my favorite thing about the project.

What started as:

"I wish there was a better event loop visualizer."

turned into a small experiment in building interactive technical education.

If you want to try it:

The Event Loop, Visualized

And if you've ever written:

setTimeout(() => {}, 0)
Enter fullscreen mode Exit fullscreen mode

as a fix and then quietly hoped nobody would ask you why it worked...

You might enjoy this project.

Top comments (0)