JavaScript Execution Context & Call Stack Interview Questions
In the How JavaScript Works Behind the Scenes article, we saw what JavaScript does before and while it runs your code. Interviewers love this topic because it explains why a lot of weird outputs happen.
Let’s test it. For every question, try to work out the answer yourself before reading the explanation.
1. What is an execution context in JavaScript?
Section titled “1. What is an execution context in JavaScript?”An execution context is the environment in which a piece of JavaScript code runs. It keeps track of the variables, the functions, and the value of this for that code.
There are two main types:
- Global Execution Context: It’s created once, when your script starts running.
- Function Execution Context: A new one is created every time a function is called.
Every execution context goes through two phases:
- Creation Phase: JavaScript scans the code and sets up memory. Variables declared with
vargetundefined, and function declarations get stored whole. - Execution Phase: JavaScript runs the code line by line and assigns the real values.
2. Why does using a var before declaring it give undefined?
Section titled “2. Why does using a var before declaring it give undefined?”console.log(city);
var city = "Delhi";
console.log(city);
Answer: undefined, then Delhi
In the above example, you can see that:
- In the Creation Phase, JavaScript sees
var cityand stores it in memory with the valueundefined. - In the Execution Phase, the first
console.logruns before the assignment, so it printsundefined. - Then
city = "Delhi"runs, and the secondconsole.logprintsDelhi.
Notice there’s no error. city exists from the start, it just doesn’t have its value yet.
3. Why does using let before declaring it throw a ReferenceError?
Section titled “3. Why does using let before declaring it throw a ReferenceError?”console.log(city);
let city = "Delhi";
Answer: ReferenceError: Cannot access 'city' before initialization
Hmm, you may be wondering why let behaves differently from var.
In the above example, you can see that:
- In the Creation Phase, JavaScript also knows about
letandconstvariables. - But unlike
var, it doesn’t give themundefined. They’re left uninitialized. - Until the line
let city = "Delhi"actually runs, touchingcitythrows aReferenceError.
This is actually a good thing. With var, a bug like this silently gives you undefined. With let and const, you get an error right away and can fix it.
4. Does every function call get its own variables?
Section titled “4. Does every function call get its own variables?”let count = 10;
function update() {
let count = 0;
count++;
console.log(count);
}
update();
update();
console.log(count);
Answer: 1, 1, 10
In the above example, you can see that:
- Every call to
update()creates a new Function Execution Context with its owncountstarting at0. - So both calls print
1. The second call doesn’t remember anything from the first one. - The
countinsideupdateis a different variable from the globalcount. It shadows the global one inside the function. - So the global
countis never touched and stays10.
5. Does a function use variables from where it’s called or where it’s written?
Section titled “5. Does a function use variables from where it’s called or where it’s written?”const value = "global";
function printValue() {
console.log(value);
}
function run() {
const value = "local";
printValue();
}
run();
Answer: global
Wait, what? printValue was called from inside run, where value is "local"!
In the above example, you can see that:
- When a function looks for a variable it doesn’t have, it looks in the place where it was written, not where it was called.
printValueis written in the global scope, so it looks there and findsvalue = "global".- The
valueinsiderunbelongs only torun.printValuecan’t see it.
This is called lexical scope, and it’s the same rule that makes closures work.
6. How does the call stack work in JavaScript?
Section titled “6. How does the call stack work in JavaScript?”function first() {
console.log("first start");
second();
console.log("first end");
}
function second() {
console.log("second start");
third();
console.log("second end");
}
function third() {
console.log("third");
}
first();
Answer:
first start
second start
third
second end
first end
In the above example, you can see that:
- The call stack keeps track of which function is running right now. It works like a stack of plates: the last one placed is the first one removed (LIFO).
first()is pushed onto the stack. It logs, then callssecond(), which gets pushed on top.second()logs, then callsthird(), which gets pushed on top.third()logs and finishes, so it’s popped off. Control goes back tosecond(), which logssecond endand gets popped.- Finally,
first()logsfirst endand gets popped.
The stack at its tallest looks like this:
| third() | ← running now
| second() |
| first() |
| Global Context |
7. What causes “Maximum call stack size exceeded” in JavaScript?
Section titled “7. What causes “Maximum call stack size exceeded” in JavaScript?”function countDown(num) {
console.log(num);
countDown(num - 1);
}
countDown(3);
Answer: It prints 3, 2, 1, 0, -1, ... thousands of times, then crashes with RangeError: Maximum call stack size exceeded.
In the above example, you can see that:
- Every call to
countDowncreates a new execution context and pushes it onto the call stack. - Nothing ever tells the function to stop, so it keeps calling itself and nothing gets popped off.
- The call stack has a size limit. Once it’s full, JavaScript throws a
RangeError. This is called a stack overflow.
The fix is a base case, which is a condition that stops the recursion:
function countDown(num) {
if (num < 0) return; // base case
console.log(num);
countDown(num - 1);
}
8. How many execution contexts does JavaScript create?
Section titled “8. How many execution contexts does JavaScript create?”function a() {}
function b() {
a();
}
b();
b();
Answer: 5 in total, and the stack is never more than 3 deep.
In the above example, you can see that:
- One Global Execution Context is created when the script starts.
- The first
b()call creates a context forb, and inside it,a()creates one more. That’s2. - The second
b()call does the same thing again. That’s another2. - So
1 + 2 + 2 = 5execution contexts are created in total. - But at any moment, the call stack holds at most
Global → b → a, which is3contexts.
Makes sense? A new context is created on every call, even when it’s the same function.
🧵 Wrapping It Up
Section titled “🧵 Wrapping It Up”You practiced how to:
- Explain execution contexts and their two phases
- Predict what happens when you use
varorletbefore declaring it - Tell which variable a function will use, based on where it was written
- Trace the call stack and explain a stack overflow
Whenever an output looks weird, ask yourself two things. What happened in the Creation Phase, and what’s on the call stack right now?