How JavaScript Works Behind the Scenes: Understanding Execution Context
What really happens when you hit “run” on your JavaScript code
You write console.log("Hello"), press run, and it works. But what actually happens between your code and the output?
Understanding this makes you better at debugging, interviews, and writing predictable code. It explains hoisting, scope, and why this sometimes behaves strangely. The concept at the center of it all is the Execution Context.
JavaScript Is Synchronous and Single-Threaded
JavaScript is a synchronous, single-threaded language. It has one thread of execution, so it runs one line at a time, in order. Everything happens inside an Execution Context.
What Is an Execution Context?
An execution context is the environment in which JavaScript code is evaluated and executed. Think of it as a big box with two components:
- Memory component (Variable Environment): stores variables and functions as key-value pairs.
- Code component (Thread of Execution): runs the code, one line at a time.
The Global Execution Context
When a JavaScript file starts running, the engine (like V8 in Chrome and Node.js) creates a Global Execution Context (GEC) first. It happens before any of your code runs, even for an empty file. The GEC also creates:
- A global object (
windowin browsers,globalin Node.js) - The
thiskeyword, which points to the global object at this level
The Two Phases of Execution Context
Every execution context is created and run in two phases.
Phase 1: Memory Creation Phase. The engine scans the code and allocates memory for variables and functions.
- Variables declared with
varare stored asundefined. - Functions are stored with their entire code.
Phase 2: Code Execution Phase. The engine goes through the code line by line, assigns the real values to variables, and runs functions.
Let’s see it with an example:
Get Rubi chauhan’s stories in your inbox
Join Medium for free to get updates from this writer.
javascript
var n = 2;
function square(num) {
var ans = num * num;
return ans;
}
var square2 = square(n);
var square4 = square(4);Memory creation phase (global):
n : undefined
square : function square(num) { ... }
square2 : undefined
square4 : undefinedCode execution phase:
n = 2is assigned.squareis skipped, because it's already stored.square(n)is called, so a new execution context is created for the function.- The function runs, returns
4, and its context is deleted. square2 = 4. The same happens forsquare4 = 16.
Function Execution Context
Every time you call a function, JavaScript creates a brand new execution context for it, with its own memory phase and execution phase. Inside square(n):
- Memory phase:
num: undefined,ans: undefined - Execution phase:
num = 2,ans = 4, thenreturnhands control back to the caller
When the function finishes, its context is destroyed.
The Call Stack
With many functions calling each other, how does JavaScript keep track of which context is running? It uses the Call Stack, a stack that follows LIFO (Last In, First Out).
- The Global Execution Context sits at the bottom.
- Each function call pushes a new context on top.
- When a function returns, its context is popped off.
Here’s the stack for this code:
function one() { two(); }
function two() { three(); }
function three() { console.log("Done"); }
one();Execution goes like this:
one()is called and pushed onto the stack.onecallstwo(), which is pushed.twocallsthree(), which is pushed.threeprints "Done" and is popped, thentwo, thenone.- Only the Global Execution Context remains. When the program ends, it is removed too.
If functions keep calling each other without ever returning (infinite recursion), the stack runs out of room and you get the famous “Maximum call stack size exceeded” error (a stack overflow).
Why This Explains Hoisting
Remember the memory creation phase? Because variables and functions are stored in memory before the code runs, you can do this:
console.log(x); // undefined (not an error!)
sayHi(); // "Hi!"
var x = 5;
function sayHi() {
console.log("Hi!");
}
x is undefined because during the memory phase var variables are initialized as undefined. sayHi() works because the whole function was stored in memory. This behavior is called hoisting. Nothing is physically "moved up", it's just a side effect of the memory creation phase.
(let and const are also hoisted, but they stay in the Temporal Dead Zone until their line executes, so accessing them early throws a ReferenceError.)
Quick Summary
- JavaScript is synchronous and single-threaded.
- Everything runs inside an Execution Context, which has a memory component and a code component.
- A Global Execution Context is created first, and every function call creates a new Function Execution Context.
- Each context runs in two phases: memory creation, then code execution.
- The Call Stack manages contexts using LIFO.
- Hoisting is a result of the memory creation phase.
Conclusion
Next time you see undefined where you expected a value, or hit a stack overflow, you'll know exactly what the engine is doing. In the next post, we'll look at how JavaScript handles asynchronous code using the Event Loop, Web APIs, and the Callback Queue.
If this helped, give it a clap 👏 and follow for more JavaScript deep dives!



