How JavaScript Runs
The call stack, the event loop, tasks, and microtasks — the machinery that decides what runs when.
So far we've written code and pressed Run, and it has Just Worked. Now we lift the hood. Understanding how the JavaScript runtime schedules your code is what separates "I think this should work" from "I know exactly why this prints in that order."
We'll focus on three structures:
- The call stack — what's currently executing.
- The task queue (a.k.a. macrotask queue) — things waiting to start.
- The microtask queue — promise reactions, very high priority.
Sitting on top of them: the event loop, the bit that ties them together.
JavaScript is single-threaded
This is the most important sentence in the whole chapter:
JavaScript runs one piece of code at a time on a single thread.
There is no parallelism happening inside your JS code. If a function takes 5 seconds, nothing else in your program runs during those 5 seconds. (We'll see why slow blocking code is a problem — and how async solves it without adding more threads.)
The call stack
When a function is called, JS pushes a stack frame for it. When the function returns, the frame is popped. That's it. The "stack" is just the list of "things in progress, most recent on top."
When main() runs, the stack grows and shrinks like this:
If you push too many frames without returning — for example, with an infinitely recursive function — you get the famous "Maximum call stack size exceeded" error.
Synchronous code blocks everything
While a function is on the stack, nothing else can run. Not timers, not network responses, not clicks — nothing.
This is exactly why long synchronous work in a browser freezes the UI — the browser also can't process clicks while the stack is busy. The fix isn't "make it faster"; it's "don't block the thread." That's what asynchronous APIs let you do.
The event loop, tasks, and microtasks
JavaScript doesn't run alone. It lives inside a host environment (a browser, Node, etc.) that provides timers, network APIs, file APIs, and so on. These hosts are written in other languages and can do work in parallel. When they finish, they hand a result back to JS by scheduling a task.
A simplified picture:
The event loop is the host's little supervisor. Its job, on repeat:
- If the call stack is empty, take the next pending microtask and run it. Repeat until the microtask queue is empty.
- Then take the next pending task from the task queue and run it (which usually means pushing a function onto the stack).
- Then drain microtasks again.
- Go to 2.
That's the whole "event loop."
- A task (macrotask) comes from
setTimeout,setInterval, I/O completions, UI events, etc. - A microtask comes from a Promise reaction (
.then/await) or fromqueueMicrotask.
The key rule: microtasks run to completion before the next task starts. That tiny rule explains a lot of surprising orderings.
A famous ordering puzzle
Try predicting the output before running.
You should see:
1: script start
2: script end
3: promise microtask
4: setTimeout callbackWhy?
1and2run synchronously, on the stack.setTimeout(fn, 0)does not runfnnow. It schedulesfnas a task.Promise.resolve().then(...)schedules its callback as a microtask.- When the synchronous script finishes, the event loop drains the
microtask queue first →
3. - Then it picks the next task →
4.
Even though both were "scheduled for as soon as possible", the microtask wins.
Async functions and await
await is sugar for "pause this function, attach the rest as a
microtask, and let other code run in the meantime."
The output is:
1: before demo()
A: before await
2: after demo()
C: after demo() -- printed lastdemo() runs synchronously up to the await. The await yields
back to the caller; "after demo()" runs because we're still on
the same stack. Only once the stack empties does the microtask
run C.
Don't worry if Promises feel hand-wavy — the next chapter is all
about them. The point right now is: await is a scheduling
hint, not magic parallelism.
Frame-by-frame execution
Here's the event-loop view of the earlier puzzle:
Reading flows like this is how you debug "why did my code print in that order?" questions.
Tiny mental model summary
- Code runs to completion on the stack before anything else runs.
- Long sync work blocks everything.
setTimeoutschedules a task; promises schedule microtasks.- Microtasks run between tasks; many can run back-to-back.
- The event loop alternates: drain microtasks → run one task → repeat.
Multi-file: scheduling demo
Predicted order:
> script begin
[sync] inside runDemo
> script end
[micro] promise A
[micro] promise B
[micro] queueMicrotask
[micro] promise B.1
[task] timeout 0
[task] timeout 0 #2All synchronous logs come first, then all microtasks (including the one B spawns), then tasks.
Challenge
Write a function schedule(log) that, when called, uses setTimeout and Promise.resolve().then to push strings into log (an array) such that, after the event loop fully drains, log equals:
["sync", "micro", "task"]
Constraints:
- Push
"sync"synchronously. - Push
"micro"from aPromise.resolve().thencallback. - Push
"task"from asetTimeout(..., 0)callback. - Use a promise that resolves once everything has been pushed, and return it. The tests will
awaitit.
Given this code:
console.log("A");
setTimeout(() => console.log("B"), 0);
Promise.resolve().then(() => console.log("C"));
console.log("D");
What does it print?
Hint: the two console.log calls are synchronous and run first; then ask which kind of callback the event loop drains first — a promise (microtask) or a setTimeout (task).
A B C D
A D B C
A D C B
A C D B