Call stack and run-to-completion: why a long function freezes the page
JavaScript runs one function at a time on a call stack, and finishes it before anything else runs. That is why a long loop freezes the page.
Comes up in: JavaScript, Recruiter screen
Learn#
Meera adds a “Generate report” button to her shop’s dashboard. It loops over 200,000 orders. While it runs, the page freezes: the spinner stops, clicks do nothing, and Chrome offers to kill the page. Nothing is broken. JavaScript is doing exactly what it promises.
When a function runs, JavaScript creates an execution context: a record of that call’s variables and where to return when it ends.
These records sit on the call stack. A call pushes a new frame on top; a return pops it off. The top frame is the only code running.
function formatPrice(n) { return "₹" + n.toFixed(2); }
function total(items) { return formatPrice(items.reduce((sum, x) => sum + x, 0)); }
function checkout(items) { console.log("total:", total(items)); }
checkout([120, 80.5]);
setTimeout(() => console.log("timer"), 0);
console.log("end of script");Output (verified)
total: ₹200.50 end of script timer
This is run-to-completion: once code starts, it runs until the stack is empty. Nothing can interrupt it — not a timer, not a click, not even a screen update.
Timers, network requests and clicks are handled by the host environment (the browser or Node.js), outside your code. The host can wait for many things at once. It only hands JavaScript a callback when the stack is empty.
Call stack
- script
Task queue
empty
Console
empty
The script starts. Its frame is the first one on the stack.
Read the steps as text
- The script starts. Its frame is the first one on the stack.
- checkout() is called. A new frame goes on top.
- checkout calls total, and total calls formatPrice. Each call adds a frame.
- formatPrice returns, so its frame is removed. Then total returns too.
- checkout prints and returns. setTimeout hands the timer to the browser and returns at once.
- The script prints its last line and finishes. The stack is empty now.
- Only now can the waiting callback run. It gets a fresh stack.
Predict#
Predict first
A click handler sets status.textContent = 'Working…' and then runs a loop that takes 3 seconds. What does the user see during those 3 seconds?
The browser only paints the screen between tasks, after the stack is empty. Your handler is still on the stack for 3 seconds, so the new text exists in the page’s data but is not drawn yet. The tempting answer assumes the screen updates the moment you change the DOM — it does not.
Interview angle#
Say it like this — about 30 seconds
“JavaScript runs on one thread with a call stack. Each call pushes a frame, each return pops one. Code runs to completion: once a task starts, nothing else runs until the stack is empty. Timers, network and events live in the host, which queues their callbacks for later. So a long synchronous function blocks everything, including painting. To stay responsive, I split heavy work into chunks or move it to a Web Worker.”
Follow-up Why don't DOM changes show up until a long function ends?
Follow-up How would you keep a page responsive during heavy computation?
Follow-up What is in an execution context?
this, and a link to the outer scope. The stack holds one per active call.Practice#
faded
sumTo below calls itself once per number, so sumTo(1_000_000) overflows the call stack. Rewrite it so it works for big inputs.
Done when
sumTo(1_000_000)returns500000500000without aRangeError.sumTo(0)is0andsumTo(5)is15.
Remember#
Try to answer each card before you open it. Then add them to your reviews.
What does the call stack hold?
One frame (execution context) per function call that has not returned yet.
What does run-to-completion mean?
Once code starts, it runs until the stack is empty; nothing interrupts it.
Why does a long loop freeze the page?
Painting and event handling wait until the stack is empty.
What does this print?setTimeout(() => console.log("B"), 0);
for (let i = 0; i < 1e7; i++) {}
console.log("A");
setTimeout(() => console.log("B"), 0);
for (let i = 0; i < 1e7; i++) {}
console.log("A");A B
Go deeper
Four layers. Stop at the one your interview needs; each ends with a quick check.
The core ideaEvery round
One stack, one frame running at a time, and every piece of code runs to the end before the next starts. If you remember only this, you can already explain most “why is my page frozen” bugs.
Check yourself: Can a timer callback run in the middle of a for loop?
No. It waits until the loop and the rest of the stack finish.
How it works underneathSenior rounds
The language spec describes an execution context stack: the running context is always the top one Source: ECMA-262: Execution contexts. The spec has no timers or clicks. Those come from the host. The HTML Standard defines an event loop that runs one task, then waits for the stack to empty before the next Source: HTML Standard: Event loops.
Check yourself: Who keeps the timer while your code runs?
The host environment (browser or Node.js), outside the JavaScript stack.
Edge cases and trapsWhere people slip
The stack has a size limit. Too many nested calls overflow it. In V8 (Chrome, Node.js) you get this error; Firefox calls it “too much recursion”:
function countDown(n) { return countDown(n - 1); } // no base case
try { countDown(10); } catch (e) { console.log(e.name + ":", e.message); }Output (verified)
RangeError: Maximum call stack size exceeded
The stack trace in the error shows the frames that were on the stack. Read it from the top.
Check yourself: What error does deep recursion cause in Chrome and Node.js?
RangeError: Maximum call stack size exceeded.
In productionStaff-level answers
A busy main thread shows up as a poor Interaction to Next Paint score, because clicks wait for the stack to empty. Here a timer waits for a 200 ms loop:
const start = Date.now();
setTimeout(() => console.log("timer waited", Date.now() - start >= 200 ? "200 ms or more" : "less"), 0);
while (Date.now() - start < 200) {} // busy for 200 ms
console.log("loop done");Output (verified)
loop done timer waited 200 ms or more
Fixes: chunk and yield (JS.01.07) or a Web Worker (WEB.06.01).
Check yourself: Name two ways to stop heavy work from freezing the page.
Split it into chunks and yield between them, or run it in a Web Worker.
Finished the five steps? Mark it, and its cards join your reviews.
Last checked 2026-10-31. Spot a mistake? Use “Report an error” below — fixes are listed on the corrections page.
Interview questions this answers
- How would you keep a page responsive while doing heavy computation?
- Why do UI updates not appear until a long synchronous function finishes?