DEV Community

Cover image for The Event Loop, Microtasks, and Macrotasks: A Visual Explanation
Alex Aslam
Alex Aslam

Posted on

The Event Loop, Microtasks, and Macrotasks: A Visual Explanation

I’ve spent the better part of a decade writing JavaScript that pretends to be synchronous. I’ve built real‑time dashboards, complex state machines, and APIs that handle thousands of requests per second. And for years, I thought I understood the event loop. I’d nod along to talks, recite “non‑blocking I/O,” and move on.

Then one night, I was debugging a bug that only happened in production. A setTimeout with 0 milliseconds was delaying a UI update just enough that a user could click a button twice. I added a Promise.resolve().then(), and suddenly the timing changed. I sat there, staring at my screen, realizing I didn’t actually know the order of things. I knew the words “microtask” and “macrotask,” but I didn’t feel them.

That night, I went down a rabbit hole that changed how I see our runtime. I stopped treating the event loop as a technical specification and started seeing it as a choreographed dance a piece of visual art that runs inside every Node.js process and every browser tab.

Let me take you on that journey. Forget the docs for a moment. Let’s look at the painting.


The Studio: Call Stack & Web APIs

Imagine your JavaScript runtime as a small, cluttered artist’s studio. In the centre is a single desk that’s the call stack. It’s a LIFO (last‑in, first‑out) stack of frames. Your code runs here, one function at a time, and it’s incredibly impatient. It can only do one thing at once.

Off to the side are the Web APIs (or Node.js APIs) think of them as the studio assistants. When you call setTimeout, fetch, or addEventListener, you aren’t actually doing the waiting yourself. You hand the task to an assistant, say “call me back when you’re done,” and immediately clear your desk for the next piece of work.

This is the first thing we internalize as seniors: asynchronous functions don’t run asynchronously; they just let you hand off work so you’re not blocked.

The Gallery: Task Queues (Macrotasks)

When an assistant finishes its work (a timer expires, a network response arrives), it doesn’t just shove the callback onto the stack. That would be chaotic the stack might be in the middle of something important. Instead, the assistant places a note on a gallery wall. That wall is the task queue (or macrotask queue).

The event loop is the curator. It watches the stack. If the stack is empty, it walks over to the gallery, picks up the oldest note (first in, first out), and places that callback onto the stack to run.

But here’s where my mental model broke that night: I thought there was one queue. There isn’t.

The gallery has multiple walls. One wall is for macrotasks setTimeout, setInterval, I/O, UI rendering events. Another, smaller, more exclusive wall is for microtasks.

The Private Collection: Microtasks

Microtasks are the VIPs of the JavaScript world. They include:

  • Promise callbacks (then, catch, finally)
  • queueMicrotask
  • MutationObserver (browser)
  • process.nextTick in Node.js (technically a separate queue, but similar priority)

When a Promise resolves, its .then callback doesn’t go to the macrotask wall. It goes to a microtask queue that sits right next to the curator’s desk.

And the curator (the event loop) has a strict rule:

After every single macrotask, before any rendering or the next macrotask, empty the entire microtask queue.

This changes everything.

The Choreography in Motion

Let’s watch a simple piece of code, not as logic, but as a ballet:

console.log('1');

setTimeout(() => console.log('2'), 0);

Promise.resolve().then(() => console.log('3'));

console.log('4');
Enter fullscreen mode Exit fullscreen mode

The performance:

  1. Stack: console.log('1') runs. Prints 1. Stack empties.
  2. Macrotask: setTimeout hands a timer to an assistant. Assistant puts callback note on the macrotask wall (after 0ms).
  3. Microtask: Promise.resolve().then schedules a microtask callback on the microtask wall.
  4. Stack: console.log('4') runs. Prints 4.
  5. Stack empty. Curator checks microtask wall. Finds the promise callback. Runs it. Prints 3.
  6. Microtask queue empty. Curator now looks at macrotask wall. Finds the timer callback. Runs it. Prints 2.

Output: 1, 4, 3, 2.

If you ever thought setTimeout(…,0) meant “run immediately after this,” you’ve been fooled by the curator’s priorities. Microtasks always cut in line.

The Frame: Rendering

In the browser, there’s an extra act. Between macrotasks, the browser may decide to repaint. But microtasks happen before that repaint. This is a critical insight for performance‑sensitive UIs.

If you schedule a massive batch of microtasks (e.g., recursively chaining promises), you can starve the rendering. The page will feel frozen because the curator is stuck emptying an ever‑growing microtask list. You’ve probably seen this as “jank.”

As a senior, you learn to spot these subtle choreographic flaws. You learn that:

  • Use setTimeout when you want to yield to the UI or give other macrotasks a chance.
  • Use queueMicrotask or Promise when you need something to happen immediately after the current synchronous code, but before the next macrotask or render.

Node.js: The After‑Hours Studio

Node.js doesn’t have a rendering phase, but it has its own quirks. It has a process.nextTick queue that is even more VIP than microtasks it gets processed before microtasks, between each phase of its event loop.

The mental model I use now: the event loop is not a simple queue. It’s a roundabout with several exits, each with different priority lanes. Understanding that roundabout has saved me from:

  • Accidentally blocking the event loop with synchronous loops.
  • Mis‑ordering critical database updates and cache writes.
  • Building reliable real‑time systems where message order actually matters.

Why This Is Art

When I finally visualized this, I stopped seeing the event loop as a dry concept. I started seeing it as a kinetic sculpture. Every await, every setTimeout, every resolved promise is a tiny marble rolling down a track. The track has checkpoints microtask checkpoints, macrotask gates, rendering frames.

The art is in the orchestration. You, the developer, place the marbles. The engine moves them with absolute consistency, but it’s your understanding of the track that determines whether the sculpture is a chaotic mess or a graceful, predictable performance.

The best full‑stack developers I know don’t just write async/await. They feel where the microtasks land. They know that an await is syntactic sugar over a promise microtask. They use setTimeout(fn, 0) intentionally to “break” a synchronous loop and let the UI breathe.

They’ve stopped fighting the runtime and started composing with it.


Your Turn to Paint

Next time you see an order‑of‑operations bug, don’t just sprinkle async keywords. Draw the queues. Ask yourself: Is this a macrotask? A microtask? Where is the render frame?

You’ll find that the more you respect the choreography, the more the engine rewards you with silky‑smooth performance and deterministic behavior.

And if you ever need to explain it to a junior, skip the slides. Walk them through a whiteboard. Draw a circle for the stack, a wall for macrotasks, a smaller table for microtasks, and a little curator with tired eyes. It’ll stick.

Top comments (0)