
How JavaScript runs in your browser comes down to a few parts working in order. An engine compiles your code. A call stack executes it one call at a time. An event loop brings slow work back when the stack is free. Your page has one thread, one loop and a few rules you can't break.
Every click, fetch and timer competes for that single worker, and somehow nothing collides. This guide follows your code through each stage in the order it meets them: the V8 engine, the call stack, browser APIs, the event loop and microtasks. By the end, you'll be able to predict exactly when every line runs and why. You'll also stop writing code that freezes clicks and scrolling. All you need is Chrome, Edge or Firefox, DevTools (F12), and a basic grasp of functions, callbacks and Promises.
- The engine parses your code into a syntax tree and JIT-compiles it before anything runs.
- The call stack is the only place your code executes. It is last in, first out.
- Timers and fetches are handed to browser APIs, and their callbacks return only when the stack is empty.
- Promise callbacks (microtasks) all run before the next timer callback.
- Never block the main thread. If work is slow, make it async or move it off the thread.
- 0:00Intro
- 0:24What you'll master today
- 1:04Parse and compile your code
- 1:49Push every call onto the stack
- 2:25Hand timers to browser APIs
- 3:00Let microtasks jump the line
- 3:35Predict the log order
- 4:10Fix the frozen page
- 4:47Power every click and live feed
- 5:21The whole journey in six lines
- 5:43One stack, one loop, never block
Parse and compile your code before it runs
Before a single line executes, the engine reads it. Engines like V8, the one inside Chrome, take your source text and turn it into something the CPU can actually run. This happens in milliseconds, every time a page loads your script. It also explains a frustrating truth: one typo can stop an entire file from running at all.
Follow one line, let x = 2 + 3, through the pipeline. First come tokens, where each piece is labelled as a keyword, identifier, punctuation or number. Then the AST, a syntax tree in which a variable declaration holds an addition. Next comes bytecode, which runs right away. Finally, code that runs often becomes optimised machine code.
Two parts do the heavy lifting. The parser checks your grammar and builds the tree. Miss one bracket and it stops right there, so nothing runs. The JIT compiler watches which code runs often and optimises it while your page is live. JavaScript was born in 1995 to make pages interactive. Today it compiles like a real language.
- Syntax error in the console? The parser stopped, so none of that code ran.
- Check brackets and parentheses first when a script does nothing at all.
- Hot code gets optimised automatically while the page runs. You don't trigger it yourself.
Push every function call onto the call stack
Once your code is compiled, it runs in exactly one place: the call stack. Every function call lands there, and your code executes nowhere else. Picture a stack of plates. You add to the top, you take from the top, and only the top plate is in play. That one rule shapes everything that follows.
Walk through three calls. When main() is pushed, the stack holds main. When main calls greet(), the stack holds main and greet. When greet calls log(), it holds main, greet and log, and log is the call running right now. When log finishes, it pops off, leaving main and greet. Then the rest pop, and the stack is empty again.
This is last in, first out. The newest call always finishes first, and its caller waits underneath. Recursion that never stops breaks this pattern. Calls keep piling on until the stack fills up, and the browser throws a stack overflow error. The stack is fast, but it has a limit.
- Only the call on top of the stack is running.
- A caller resumes only after the function it called returns.
- Every recursive function needs an exit condition.
Hand timers and fetches to the browser's Web APIs
If the stack runs only one thing at a time, who waits on a timer or a network request? Your code doesn't. The browser does. Slow work never sits on your stack. The stack hands it off, keeps going, and the event loop brings the result back later. That handoff is how one thread juggles clicks, fetches and timers without collisions.
Follow a setTimeout. The call stack passes the timer to the browser's Web APIs and moves straight on to the next line. When the time is up, the callback joins the task queue. It does not jump onto the stack right away. The event loop pushes it there only when the stack is empty.
That last detail matters. A zero isn't instant: a 0 ms timer still waits until every line currently on the stack has finished. Fetch works the same way. The browser waits on the network off the stack, so your thread stays free to respond to clicks while the data is on its way.
- A setTimeout delay is a minimum, not an exact time.
- Callbacks reach the stack only when it is empty.
- Network waits happen off the stack and don't block your thread.
Let promise microtasks jump the line
Here's the plot twist: not every callback waits in the same line. Promise callbacks go into a microtask queue, and that queue gets priority. Microtasks run before the next timer callback fires, even when the timer was scheduled first. This rule surprises a lot of developers, and it explains many order-related bugs.
One full turn of the event loop has four stages. First, take a task, such as a timer callback. Second, run it to the finish, until the stack is empty. Third, drain the microtasks, which means every pending promise callback, not just one. Fourth, paint the screen if needed. Then the loop starts again with the next task.
Callbacks from .then and await go into this fast lane, and every one of them runs before the loop moves on. Timers have to wait. Even a timer that was due first gets in line behind every queued promise callback.
- .then and await callbacks go into the microtask queue.
- setTimeout callbacks go into the task queue.
- All microtasks drain before the next task starts.
- The screen paints after microtasks, if it needs to.
Predict the log order in your console
Now test the model. This snippet has four logs in three different lanes: two synchronous logs, one timer and one promise. Paste it into the DevTools console, but write down your guess first. Guessing is the point, because it forces you to apply each rule in order.
Line one logs right away. The setTimeout goes to the browser. The promise queues a microtask. Line four logs right away too. So the console shows 1 sync, then 4 sync. Next comes 3 promise, because microtasks drain as soon as the stack empties. 2 timer comes last. The promise beat the zero-millisecond timer.
Then push further. Add a second timer, or chain another .then onto the promise. Swap lines around. Predict the output each time before you run it. If your guesses keep matching the console, you've got the model.
- Synchronous logs print first, in order.
- Promise callbacks print next.
- Timer callbacks print last, even at 0 ms.
- Change one line, predict again, then check.
console.log('1 sync');
setTimeout(() => console.log('2 timer'), 0);
Promise.resolve().then(() =>
console.log('3 promise'));
console.log('4 sync');
Fix the frozen page and other event loop bugs
When something goes wrong at this level, it's almost always one of four bugs. The biggest one is blocking the main thread. You click a button and nothing happens. Scrolling is stuck. The cause is one loop that is still running and holding the stack, so the event loop can't deliver your click or paint the screen.
Match your symptom to a fix. For a frozen UI caused by a heavy loop, split the work up or move it to a Web Worker. For a late timer that fires after more than 0 ms, treat the delay as a minimum. For the wrong order, where you expected the timer before the promise, remember that microtasks always run first. For a stack overflow, add an exit to your recursion.
All four fixes follow one main thread rule: if work is slow, don't run it synchronously. Make it async or move it off the thread. Keep the stack clear, and the loop keeps your page responsive.
- Frozen UI: split heavy loops or use a Web Worker.
- Late timer: treat the delay as a minimum.
- Wrong order: microtasks run before timers.
- Stack overflow: give recursion an exit.
Apply the model to every click and live feed
This isn't trivia. The same loop runs behind the interactive pages you use every day, from Gmail to Google Docs. Click handlers, live feeds, Gmail's inbox, typing in Google Docs and every fetch call all pass through it. When the loop stalls, all of them stall together.
JavaScript was born in 1995 to make pages interactive. Now it runs full applications. The stakes are real, because users never see your code. They see a tab that responds or a tab that freezes. A frozen tab feels broken, and a smooth loop is what makes an app feel trustworthy.
Here is the whole journey in order. The engine parses and JIT-compiles your code. The call stack runs it, last in, first out. Browser APIs handle timers and fetches. The event loop feeds callbacks back. Microtasks run before the next timer. And you never block the main thread.
The bottom line
- The engine parses and JIT-compiles your code. A syntax error stops everything.
- The call stack is last in, first out, and it is the only place code executes.
- Browser APIs handle timers and fetches off the stack.
- The event loop returns callbacks only when the stack is empty.
- Microtasks from .then and await run before the next timer.
- Never block the main thread. Make slow work async or move it off the thread.
Quick-fire questions
Why doesn't setTimeout with 0 ms run immediately?
The timer goes to the browser's Web APIs, and its callback waits in the task queue. The event loop moves it onto the stack only after every line currently on the stack has finished and all microtasks have drained. Treat the delay as a minimum, not an exact time.
Do promises run before setTimeout?
Yes. Promise callbacks from .then and await are microtasks. Each turn of the event loop drains all pending microtasks before it takes the next task, so a promise callback runs before a timer callback, even a 0 ms timer that was scheduled first.
What is the call stack in JavaScript?
It is where your code actually executes, one call at a time. Each function call is pushed on top, and the newest call finishes first while its caller waits underneath. When the stack is empty, the event loop can hand it the next callback.
Why does my web page freeze when JavaScript runs?
A heavy synchronous loop is holding the main thread. While it runs, the event loop can't process clicks or paint the screen. Split the work into smaller pieces or move it to a Web Worker.
What causes a stack overflow error in JavaScript?
A stack overflow happens when recursion never stops. Calls keep piling onto the call stack until it fills up and the browser throws an error. Adding an exit condition to the recursive function fixes it.
What does the JIT compiler do?
The JIT compiler watches which code runs often and optimises it into machine code while your page is live. Code first runs as bytecode right away, and the most frequently run parts get faster as the page keeps running.