How JavaScript Works Behind the Scenes
You’ve written variables, functions, and conditionals. Your code runs and produces output. But have you ever wondered what actually happens when JavaScript executes your code?
Where do variables live before they’re assigned? Why can you call a function before it’s defined in some cases? What creates that “environment” where your code runs?
This article pulls back the curtain and shows you how JavaScript runs your code under the hood.
Analogy
Section titled “Analogy”Imagine you’re directing a play. Before the actors say a single line, the crew has to set up the stage. They place props, mark positions, and prepare everything. Only after that does the actual performance begin.
JavaScript works in a similar way. Before any line of your code runs, the engine sets up an environment. It reserves space for variables, stores function definitions, and prepares the stage. Then, and only then, does it start executing your code line by line.
That setup and execution happen inside something called an Execution Context.
What is an Execution Context?
Section titled “What is an Execution Context?”An Execution Context is the environment where your JavaScript code runs. It holds everything the code needs: variables, functions, and a reference to the outer scope.
Whenever you run JavaScript:
- A Global Execution Context is created for your entire script.
- Each time you call a function, a new Function Execution Context is created for that function.
Think of it as a sandbox. The global context is the main sandbox. Each function gets its own smaller sandbox when it runs, with its own variables and parameters.
The Two Phases of Execution
Section titled “The Two Phases of Execution”JavaScript doesn’t just run your code top to bottom in one pass. It works in two phases:
- Creation Phase (also called the Memory Phase)
- Execution Phase
Let’s break each one down.
Phase 1: Creation Phase
Section titled “Phase 1: Creation Phase”In the Creation Phase, JavaScript sets up the environment before running a single line of your code.
It does the following:
-
Creates the Global Object: In browsers, this is
window. In Node.js, it’sglobal. Your entire script runs in the context of this object. -
Sets up
this: Thethiskeyword is bound to the global object (or the object that owns the current context). -
Creates the Memory Heap: A space where variables and function references are stored.
-
Hoists variables and functions: All
vardeclarations and function declarations are scanned. Variables are initialized toundefined. Functions get their full definition stored.
This is why you might have seen “weird” behavior like this:
console.log(name); // undefined (not an error!)
var name = "John";
The variable name exists in memory during the Creation Phase, but it holds undefined until the Execution Phase assigns "John" to it. This behavior is called hoisting.
For let and const, the variable exists but is in a “temporal dead zone” until the line that declares it runs. Accessing it before that throws a ReferenceError.
Phase 2: Execution Phase
Section titled “Phase 2: Execution Phase”In the Execution Phase, JavaScript runs your code line by line. It assigns values to variables, executes function calls, and evaluates expressions.
When the engine hits a function call, it pauses the current execution, creates a new Execution Context for that function, runs through both phases for that function, and then returns the result back to where it was called.
Walking Through an Example
Section titled “Walking Through an Example”Let’s trace through a simple example step by step.
let val1 = 10;
let val2 = 5;
function addNum(num1, num2) {
let total = num1 + num2;
return total;
}
let result1 = addNum(val1, val2);
let result2 = addNum(10, 2);
Step 1: Global Creation Phase
Section titled “Step 1: Global Creation Phase”Before any code runs, the engine sets up the global context:
| Variable/Function | Initial Value |
|---|---|
| val1 | undefined |
| val2 | undefined |
| addNum | function definition |
| result1 | undefined |
| result2 | undefined |
All variables are in memory with undefined. The function addNum is stored in full.
Step 2: Global Execution Phase
Section titled “Step 2: Global Execution Phase”Now the code runs line by line:
Line 1: val1 = 10
Line 2: val2 = 5
Line 3-7: function addNum -> skipped (already in memory)
Line 9: result1 = addNum(val1, val2) -> function call!
When the engine hits addNum(val1, val2), it pauses and creates a new Execution Context for the function.
Step 3: Function Context for addNum(val1, val2) (First Call)
Section titled “Step 3: Function Context for addNum(val1, val2) (First Call)”Creation Phase for this function:
| Variable | Initial Value |
|---|---|
| num1 | undefined |
| num2 | undefined |
| total | undefined |
Execution Phase for this function:
num1 = 10 (from val1)
num2 = 5 (from val2)
total = 10 + 5 = 15
return 15
The function returns 15 to the global context. This function’s Execution Context is then removed from the stack. Control returns to the global context.
result1 = 15
Step 4: Function Context for addNum(10, 2) (Second Call)
Section titled “Step 4: Function Context for addNum(10, 2) (Second Call)”Creation Phase for this function:
| Variable | Initial Value |
|---|---|
| num1 | undefined |
| num2 | undefined |
| total | undefined |
Execution Phase for this function:
num1 = 10
num2 = 2
total = 10 + 2 = 12
return 12
The function returns 12. Its context is removed. Control returns to global.
result2 = 12
Step 5: Back to Global
Section titled “Step 5: Back to Global”The global execution continues. Both result1 and result2 now have their values. When the script finishes, the Global Execution Context is also cleared.
The Call Stack
Section titled “The Call Stack”JavaScript uses a Call Stack to manage Execution Contexts. Think of it as a stack of plates:
- When a function is called, its context is pushed onto the stack.
- When the function returns, its context is popped off the stack.
- The context on top of the stack is the one currently running.
This is why you see a stack trace when an error occurs. It shows you the chain of function calls that led to the error.
Why This Matters
Section titled “Why This Matters”Understanding Execution Context helps you make sense of:
- Hoisting: Why
varand function declarations seem to “move” to the top. - Scope: Why variables inside a function aren’t visible outside.
- Closures: How functions “remember” the environment they were created in (we’ll cover this in a later article).
You don’t need to think about Execution Context every time you write code. But when something behaves unexpectedly, this mental model will help you debug it.
Wrapping It Up
Section titled “Wrapping It Up”JavaScript runs your code in two phases: Creation and Execution. Before any code runs, the engine sets up an Execution Context, reserves space for variables, and stores function definitions. Then it executes your code line by line. Each function call gets its own Execution Context, which is managed by the Call Stack.
You learned how to:
- Understand what an Execution Context is
- See how the Creation Phase hoists variables and functions
- Trace through the Execution Phase step by step
- Relate this to hoisting, scope, and the Call Stack
Keep this model in mind when you encounter hoisting quirks or scope-related bugs. It will save you a lot of head scratching.