The event loop: one thread, one task at a time
JavaScript runs your entire application on a single thread, and yet it handles clicks, timers, and network responses without freezing — the trick is a loop that runs one task to completion, then picks up the next. This lesson builds that model (call stack, queues, the loop) from scratch, because it is the hidden engine under every async feature and every jank bug you will ever debug.
The event loop: one thread, one task at a time
Almost every confusing thing about JavaScript execution — why a setTimeout(fn, 0) doesn't run now, why a heavy loop freezes the whole page including animations, why a Promise callback jumps ahead of a timer, why await "pauses" a function without blocking anything else — comes from one piece of machinery that the language never makes you look at directly: the event loop. Learn it once, properly, and all of those stop being trivia to memorize and become things you can derive.
The core fact to start from: JavaScript runs on a single thread. It can only do one thing at a time. The event loop is the mechanism that lets that one thread juggle many pending pieces of work — by running them one at a time, each to completion, in turns. Everything below unpacks that.
One thread, and the problem it creates
"Single-threaded" means there is exactly one call stack, and exactly one thing executing at any instant. There is no "meanwhile, on another thread" in your JavaScript by default. This is a deliberate choice — it means you never have two pieces of your code touching the same variable at the same time, so the entire category of data-race bugs that plague multithreaded languages simply doesn't exist here. Your code is never interrupted mid-function.
But that immediately raises a problem. If there's one thread and it runs your code start to finish, how does a page stay responsive? A network request can take two seconds — does the browser freeze for two seconds? A click handler, a timer firing, a fetch completing — these happen at unpredictable times. With one thread and no way to "wait for something" without stopping everything, you'd be stuck. The event loop is the answer, and its answer is: don't wait — schedule.
The model: a stack, a queue, and a loop
Three pieces. Hold all three and you have the whole thing.
The call stack is where code actually executes. When you call a function, a frame is pushed; when it returns, the frame pops. While there are frames on the stack, the thread is busy running them, top to bottom, with no interruption. The stack is the "one thing at a time."
The task queue is a line of pending pieces of work waiting to run — each one a function to be executed. When you click a button, the browser doesn't run your click handler immediately in the middle of whatever's executing; it puts a task ("run the click handler") into this queue. Same for a timer that has elapsed, or a network response that has arrived.
The event loop is the absurdly simple rule that connects them: when the call stack is empty, take the next task from the queue and run it — pushing its function onto the stack and letting it run to completion. Then, when the stack is empty again, take the next task. Forever.
That loop — "stack empty? run next task; repeat" — is the event loop, and it is running constantly under every web page. Your whole program is a series of tasks pulled off that queue and run to completion on the one stack.
Run to completion: the rule that explains the jank
Here is the single most important consequence, and it's worth stating starkly: once a task starts running, it runs all the way to completion before the event loop can pick up anything else. JavaScript will never pause your function partway through to go run a click handler or a timer. Whatever is on the stack finishes first, always.
This is a gift and a curse. The gift: you never have to worry about your code being interrupted mid-update, leaving data half-changed. The curse: if one task takes a long time, nothing else can happen during it — no other timers fire, no clicks are processed, and — crucially — the browser can't render a frame. A single synchronous loop that runs for 500ms freezes the entire page for 500ms:
// This freezes the page solid for its whole duration.
const start = performance.now();
while (performance.now() - start < 500) {
// busy work — the stack never empties for 500ms
}
// During these 500ms: no clicks handled, no timers fired, no frames
// rendered. The event loop is stuck waiting for this task to finish.If you've read the frame-budget lesson, this is the same truth from the other side: "a long task drops frames" is "run-to-completion means the loop can't get to the render step until the task ends." One thread, one task at a time — so every task you run is a task during which nothing else, rendering included, can happen.
"Asynchronous" does not mean "parallel"
The word "async" misleads a lot of people into thinking a second thread is involved. It usually isn't. When you call setTimeout(fn, 1000) or fetch(url), here's what actually happens: your JavaScript hands the waiting part off to the browser — which does have other threads and OS facilities for timers and network — and then your JS immediately continues to the next line. It does not block. Later, when the timer elapses or the response arrives, the browser doesn't interrupt you; it drops a task ("run this callback") into the queue, and the event loop will get to it when the stack is next empty.
So the asynchrony is not "your code runs in parallel." Your code still runs one task at a time on one thread. Asynchrony just means the callback runs later, as a future turn of the loop, instead of now. This is why:
console.log('A');
setTimeout(() => console.log('B'), 0);
console.log('C');
// Logs A, C, B — never A, B, C.Even with a 0ms timeout, B can't run until the current task (the top-level script that logs A and C) runs to completion and the stack empties. setTimeout doesn't mean "run now"; it means "enqueue a task to run in a future turn." The 0 is a minimum delay before it's eligible, not a promise it runs immediately.
Step through it yourself
Reading about the stack and the queue is one thing; watching a program move through them is another. Below, step through a small script and watch the call stack fill and empty, tasks wait their turn, and the console output appear in the order the loop actually produces it — not the order the code is written.
console.log('1');setTimeout(() => console.log('2'), 0);Promise.resolve().then(() => console.log('3'));console.log('4');
The top-level script runs as the first task, on the call stack.
The synchronous script always runs to completion first. Only once the call stack is empty does the loop drain every microtask — promise callbacks, mostly — before it even looks at the task queue. Only after the microtask queue is empty does it take the next task (a timer, an I/O callback, a UI event). That ordering — run the task, drain all microtasks, then take the next task — is why the resolved promise's .then (3) prints before the zero-delay setTimeout (2), even though both were "ready" at the same moment.
The thing to internalize is the rhythm: the synchronous code runs first as one task (filling and emptying the stack), and only then does the loop start pulling the deferred callbacks off the queue, one turn at a time. That rhythm is the event loop, and it's the same rhythm whether the deferred work came from a timer, an event, or a network response.
Where this goes next
We've leaned on "the call stack" as if it's obvious, but the stack is where "run to completion" and "blocking" physically live, and it's worth a close look on its own. The call stack and run-to-completion zooms into a single task: how frames push and pop, what "the stack is empty" precisely means for the loop, what a stack overflow actually is, and why the depth and duration of one task is the whole ballgame for responsiveness.
Go deeper
- MDN — The event loop — The call-stack / task-queue / run-to-completion model this lesson builds, from the reference.
- Philip Roberts — What the heck is the event loop anyway? — The classic talk that animates the stack, Web APIs, and the queue exactly as this lesson describes them.
- WHATWG HTML spec — Event loops — The authoritative definition of the loop, its task queues, and the processing model the later lessons formalize.
Check yourself
Answer out loud, as if an interviewer asked. If you hand-wave, reread that section.
- What does 'single-threaded' concretely mean for JavaScript, and what entire category of bugs does it eliminate?
- State the event loop's rule in one sentence, naming the call stack and the task queue.
- Define 'run to completion' and use it to explain why a 500ms synchronous loop freezes the whole page, rendering included.
- Tie this to the frame-budget lesson: restate 'a long task drops frames' in event-loop terms.
- Why is 'asynchronous' not the same as 'parallel'? Walk through what setTimeout actually does to your one thread.
- Explain precisely why `console.log('A'); setTimeout(()=>console.log('B'),0); console.log('C')` prints A, C, B — including why the 0ms delay doesn't make B run immediately.
- The lesson says the single-queue model is a simplification. What two refinements are coming, and which features do they explain?