Contexto
With 3 or 4 years of hands on experience, working directly and daily in the front end world, at US companies, I got invited to interview for a mid level role, for a Next.js project. I was literally doing exactly that at the time, at the company I was working for, so I decided to give it a shot. After all, I wasn’t dead: the market was showing signs of coming back, and I’ve always had this philosophy of staying visible.
So: I passed the normal interviews and, in the first technical one, I answered everything React without major issues. However, 4 questions purely about JavaScript left me completely lost, I wasn’t confident enough to even guess the right output, let alone explain how to get there. One of those questions was similar to the example below:
console.log("A");
setTimeout(() => {
console.log("setTimeout A");
}, 0);
async function runA() {
console.log("PRE await A - runA");
await null;
console.log("POS await A - runA");
}
runA();
for (let i = 0; i < 2; i++) {
console.log(`For loop A - ${i}`);
}
What does this code print?
A
PRE await A - run A
For loop A - 0
For loop A - 1
POS await A - runA
setTimeout A
My first thought when I saw that question was:
Why are they asking me this? In all my years in Front-End I never had to stop and think about the exact order of logs when there’s an await in the middle of a function and other similar scenarios. I just wrote the code, tested it, and moved on.
On one hand, that might be true in the most basic scenarios. It might even be true in more than 90% of cases, but still, despite the anger, the result was clear: I didn’t pass, because my performance on the test wasn’t good enough.
on’t want to get into whether that testing methodology is actually beneficial or not. What matters is: the job wasn’t mine, and the game keeps going. So that day, I opened Filipe Deschamps course, Curso.dev, and in one of the lessons, while talking about authentication, instead of jumping straight into code, he took a much longer route to explain the foundation: the different ways we can handle passwords, the trade offs involved, what encryption actually is, and at the end, he showed a very simple piece of code that, after I understood the base, made perfect sense.
Every piece clicked in a beautiful, direct way and, with the puzzle assembled, I understood why the course chose that methodology for the project and I immediately carried it into other use cases from projects I’d already done.
That lesson sparked something in me, a kind of seed of indignation. It made me remember the interview earlier and think: how can someone who’s already working in the market, delivering good work that gets praised by the front end teams they’re in (or have been in), not know the basics of JavaScript? Maybe it’s because I came straight from Python and decided to skip the JavaScript foundation? Maybe. It barely matters. I decided to study.
In this post I’m going to talk a bit about how things didn’t make sense to me at first and how the map started coming together. I built a playground specifically so I could visualize the movement of code blocks and lines, and you’ll be able to play with it at the end.
But what the hell is this?
Everywhere I looked, explanations started like this:
JavaScript executes code on a single Call Stack, in a single threaded way, per context, and it’s the Event Loop’s responsibility to coordinate async calls with Microtasks and Macrotasks.
Right away, one of the first questions I asked when I ran into these explanations was:
And how does the Call Stack know what to run? Who has priority in the eyes of the Event Loop? What does synchronous and asynchronous actually mean?
Alright. Let’s go piece by piece.
Before anything: the “characters” in this story
So the text doesn’t turn into a soup of loose terms, let’s separate a tiny legend right here at the start.
- JavaScript Engine: Executes JavaScript code, pushing and popping frames on the Call Stack.
- Environment (Node or Browser): Provides APIs like
setTimeout, I/O, fetch, etc. Even though the names differ between them, we’ll call these APIsHost APIsto keep it simple. - Event Loop: The coordinator that decides when callbacks enter execution and enforces the ordering and priority rules between queues.
Sync & Async
In a very simplified way, synchronous is what can be resolved right now on the Call Stack, with data already in memory, and doesn’t require the engine to come back to it later. Operations, however, that schedule a continuation for the future, either by time (setTimeout), or because they only run when the user does a specific action (example: the callback of a button onClick), are called asynchronous.
To be objective, let’s classify a few groups and objects:
| Function | Síncrono | Microtask | Macrotask |
|---|---|---|---|
| console.log('.') | ✅ | ||
| Promise.resolve().then() | ✅ | ||
| setTimeout() | ✅ | ||
| for loop | ✅ | ||
| await 1 | ✅ |
"Even when awaiting simple values like null, await pushes the continuation of the function into the microtask queue."
Does the Call Stack know anything?
After hearing and reading “Call Stack” over and over, I started wondering if it knew anything at all and what mechanisms it had to differentiate calls, how it identifies what runs when, and what the execution priority is between microtasks and macrotasks.
It’s important to emphasize: the Call Stack doesn’t know absolutely anything. It’s dumb. The stack is exactly what it claims to be: just a stack of executions (LIFO2).
The one that defines ordering rules and when callbacks can run is the Event Loop (inside the execution model of the environment). The Call Stack works like a premium runway: while there is synchronous code running, nothing outside it executes, not until it’s empty.
Event Loop, the brain
the simplest little piece in this whole equation, and it has one very simple job. (...) The event loops job is to look at the stack and look at the task queue. If the stack is empty it takes the first thing on the queue and pushes it on to the stack which effectively run it. — JS Conference - Phillip Roberts: https://www.youtube.com/watch?v=8aGhZQkoFbQ
The Event Loop is the central mechanism of JavaScript. When the code is interpreted for the first time, we go through the context creation process, where global and local function contexts are built.
This is where let, const, var, function are identified and stored in their respective places, with hoisting, placement in the TDZ (Temporal Dead Zone),
and references to other scopes are created. In the second part, we enter execution for real.
JavaScript runs line by line, function by function, in the order they’re called, pushing and popping the Call Stack. When that stage ends, meaning the Call Stack has nothing else to execute and becomes empty, the Event Loop becomes the master of ceremonies.
The Event Loop will pick and drain absolutely everything in the Microtask Queue first, without touching the Macrotask Queue. The priority is always this:
Call Stack -> Microtask Queue -> Macrotask Queue.
Why is it important to know that? Because it explains, for example, why Promises jump ahead of setTimeout, even when the timeout has a very short delay, even 0ms.
setTimeout(..., 0) doesn’t mean immediately. It means it needs to run immediately as soon as it can, when we reach pending macrotasks.
Where do setTimeout and Promise.then(...) come in?
Line by line, the JS engine executes the script and builds stacks according to its rules.
When we hit, for example, a setTimeout, the JavaScript engine won’t execute the callback immediately, like it would synchronously. It will register
that call in the Host API for the requested time (for example, 1000 ms), and when that time expires, that call will be pushed into the Macrotask Queue.
In the same way, when we go through a Promise.resolve().then(), the Promise.resolve() resolves immediately, and the callback of .then(...) gets scheduled in the Microtask Queue.
Meaning: the resolve() happens now; the .then() runs later, as a microtask, when the stack empties.
So here we’ve seen a scenario where the JavaScript engine is placing calls onto the Call Stack, the Microtask queue, and the Macrotask queue.
Where does the “wisdom” and decision making of the Event Loop come in?
As soon as the Call Stack empties, it goes to its second priority list: the Microtask Queue. Each microtask is then pushed onto the Call Stack and executed, resolving whatever is inside each callback. This draining of microtasks into the Call Stack is done by the Event Loop, and later, the draining of the Macrotask Queue. Worth repeating: the Event Loop doesn’t execute code, it only decides when each callback can enter the Call Stack. The actual execution is done by the JS engine. It took me a while to really internalize this separation between the Event Loop and the Engine.
Example
Below you’ll see an example in the sandbox I built. Here you won’t be able to edit the functions, only the animation speed. At the end of the text you’ll have a full sandbox to play with, manipulate functions, and test any ordering and any number of functions to understand JavaScript’s step by step for these basics.
Estado inicial
So… do I actually need to know this?
Knowing these details, about JavaScript’s execution order, can become very important in projects, for example
when performance becomes a deciding factor. Understanding that the UI might be freezing because we have a huge
volume of microtasks, making each macrotask sit in a long wait before it can run. Or why a .then()
“cuts the line” and shows up before setTimeout(0). Why do many chained Promises feel like they can (or do) freeze the UI?
This kind of foundational knowledge will implicitly show you that you do need to know these details, even if it isn’t the most direct thing and won’t always show up in your face as a standard interview question.
Now, before we go to the Playground, try to mentally answer the call order below:
setTimeout(() => console.log("T"), 0);
Promise.resolve().then(() => console.log("P"));
console.log("S");
Answer
text S P T
Playground
Footnotes
-
awaithere is being considered in the sense of “what happens when, for example, inside a function we have anawaitand there are synchronous logs before and after it”. Even when youawaita simple value (like null), JavaScript treats it likeawait Promise.resolve(null). In practice, that pushes the continuation of the function to the microtask queue.Example:
↩async function runA() { console.log("PRE await A - runA"); await null; console.log("POS await A - runA"); } runA(); -
LIFO: Last In, First Out ↩