You watch a conference talk about the JavaScript event loop. Someone draws boxes on a whiteboard, or a newer explainer animates two colored queues. setTimeout versus Promise.then suddenly makes sense. Two weeks later a teammate pastes a four-line snippet in review and asks what logs first, and the order falls apart in your head.
I've been through that cycle more times than I want to admit. The event loop isn't hard because the pieces are fancy. It's hard because they only make sense together, and most write-ups walk you through them one at a time, then never ask you again.
This is the version that finally stuck for me, and how I keep it from leaking out between interviews and late-night bugs.
The call stack runs one thing at a time
JavaScript in the browser (and the mental model most of us use in Node) has a single call stack. A function starts, it may call another function, and when a function returns it pops off. While the stack still has frames on it, nothing else in your script runs. That's why a tight while (true) {} freezes the tab. The loop never leaves the stack, so the browser never gets a turn to paint or handle a click.
Async does not mean another thread is running your callback. It means you'll schedule the work and come back to it after the stack is clear.
Macrotasks are the bigger tickets
A macrotask (the HTML spec just calls them tasks) is a unit of work the event loop picks up: the original script, a setTimeout callback, a setInterval tick, a click handler, an I/O callback in Node. The loop takes one macrotask, runs it until the stack is empty, then decides what comes next.
setTimeout(fn, 0) does not mean "run fn immediately." It means put fn on the macrotask queue and run it after the current task finishes, after any tasks already waiting, and after the delay has elapsed. If the stack is busy, zero can be a lot longer than zero.
Microtasks cut in before the next macrotask
This is the part I used to mix up with timeouts.
When the stack empties, the runtime drains the entire microtask queue before it takes the next macrotask. Promise callbacks (.then, .catch, .finally), queueMicrotask, and MutationObserver all land here. In Node, process.nextTick runs even earlier than the other microtasks, which is its own small trap.
So this logs A, then C, then B:
console.log("A");
setTimeout(() => console.log("B"), 0);
Promise.resolve().then(() => console.log("C"));
A runs now, on the current stack. The timeout is a macrotask. The promise callback is a microtask. After A finishes, every waiting microtask runs (C), and only then does the timeout get a turn (B).
If a microtask queues another microtask, that one runs too, before any macrotask. A long chain of Promise.resolve().then(...) can keep the UI waiting the same way a tight while loop can. I've seen that surprise people who assumed anything async would keep the page responsive.
The picture I keep in my head
One stack. Two queues. After the stack clears: empty every microtask, then take one macrotask, then empty the microtask queue again.
Clicks, timeouts, and I/O wait in the macrotask line. Promise reactions cut in between them.
If I can walk a four-line snippet against that picture without opening a tab, I actually know it.
Why the talk fades by the next interview
You learned this the usual way: a talk, a blog with colored boxes, maybe a sandbox you closed the same afternoon. The model was clear for a day. Then you went back to writing async/await (which is promises underneath, so microtasks), and nothing asked you about the queues until a whiteboard did.
Re-watching the same video feels like studying and isn't. You recognize the animation. Recognition is not the same as predicting the log order of a snippet you haven't seen before.
What helps is writing the two-queue picture in your own words, then being asked about it a few days later, before you need it in an interview or a race you can't reproduce.
I started saving the talks and articles that actually explained a piece into Recl. It takes a video or an article, gives you a short summary and key takeaways, and files the save under a topic. I add my own note on top: "after the stack clears, drain every microtask, then one macrotask. setTimeout is a macrotask. .then is a microtask. nextTick in Node runs before other microtasks."
That note is what I want the next time someone asks why setTimeout(fn, 0) ran after a chain of promises.
Let a question come back later
A few days later Recl nudges you: "Still remember this one?" The question is usually something small, like which queue Promise.then uses. If I answer it, it comes back in about a week. If I blank, I read my note and see it sooner.
Index cards work too. I just got tired of maintaining a separate deck for every JavaScript topic I keep relearning. It's easier when the prompt comes from the talk I already saved.
A loop you can run this week
- Predict the log order of this snippet, then run it:
console.log(1); setTimeout(() => console.log(2), 0); Promise.resolve().then(() => console.log(3)); console.log(4); - Change the promise callback so it queues another
.thenthat logs5, and predict again. - Write, in one sentence, what happens after the call stack empties.
- In a few days, before you open a diagram, try to explain why
setTimeout(fn, 0)can still lose to a promise. Check yourself.
You don't need to memorize the HTML spec's processing model. You need the relationship between the stack, microtasks, and macrotasks to still be there the next time a snippet looks out of order, and a way to pull that picture back up on purpose.
If you want to see what summarized takeaways look like on real material, the public Recl library has a few hundred of them.
I'm Asutosh, and I build Recl, an app for saving videos, articles and notes, getting the key ideas, and recalling them on a schedule so they actually stick.


Top comments (1)
i love this.