Understanding how JavaScript code is executed is crucial whether you are learning the language or working with it regularly. To truly understand how hoisting works, why a ReferenceError is thrown, or in what order your code executes — these are just a few reasons why this concept is important.
First things first, always remember that JavaScript is a single-threaded, synchronous language. Now what does that mean? In simple terms, single-threaded means JavaScript has only one call stack and can execute one task at a time. This execution happens synchronously i.e. code is executed one ‘after’ the other. The thread of execution executes a line at a time, ‘waits’ for it to complete and then executes the next line.
Note: JavaScript executes code synchronously by default, but can handle asynchronous operations using the event loop, Web APIs, callbacks, promises, and async/await.
Note: The call stack follows the Last In, First Out (LIFO) principle.
What is an Execution Context ?
JavaScript code is executed in an environment called Execution Context. So, whenever a JavaScript program or script runs, an execution context is created to execute the JS code.
Note: An execution context is also created within the main context when a JavaScript function is called or invoked.
There are two parts in an execution context:
Memory part (variable environment): This is where all variables, constants and functions are stored as key-value pairs (we will see how exactly in some time :)
Thread of execution: This is where code is executed one line at a time, one after the other.
There are two types of execution context:
Global Execution Context (GEC): This is the main execution context created for the program, as soon as it starts executing. It contains a global memory and a thread of execution. All top-level code runs inside the Global Execution Context. Remember there is only one global execution context per program. This context is deleted after the program is fully executed.
Function Execution Context (FEC): Whenever a function is called or invoked (recognised by the parenthesis next to the function name eg. sum(2,3) or showSum(), a new execution context for that function specifically is created inside the global execution context. This function execution context has its own memory space known as local memory and its own thread of execution where every line of code in the ‘function definition’ (till return) is executed. This context is deleted once the return statement in the function is executed or after execution of the last line in the function body (in case there is no return statement) whereupon JavaScript will implicitly return undefined.
Note: A function execution context has access to its local memory and also the global memory! But the Global execution cannot access a local memory. The global context does not have access to variables declared inside a function, because they are scoped to that function.
An Execution context is created in two phases:
Phase 1- Memory creation phase: This is the phase before the code is actually executed. Initially, each line in the script is parsed and memory is allocated for every defined variable, constant and function. During the memory creation phase, var variables are initialised with undefined. Let and const declarations are hoisted but remain uninitialised in the Temporal Dead Zone until their declaration is executed. Function declarations are fully hoisted with their function definition. During parsing (before execution), syntax errors are detected.
Phase 2-Code execution phase: Once the memory is allocated in the global space or memory of global execution context, the code is executed in this phase line by line from first line to last, one line after another, one line at a time. This is where thread of execution is created and actual code execution takes place i.e., all calculations are made and resulting values are assigned to existing labels in memory and function calls are executed (leading to creation of function execution context which again follows these two phases).
Note: Why did I emphasise above that the values are assigned to existing labels ? What happens if there are no labels i.e. no allocated memory?
'use strict';
var num = 5;
numSquare = num * num; // ReferenceError at runtime
Consider above example, to understand what I meant when I said ‘values are assigned to existing labels in memory’ . What will be the output of the above code? Yes, it will throw a ReferenceError: numSquare is not defined. Well, most of you already know what this means — we are trying to refer to a variable named ‘numSquare’ that is not even defined, hence the error. But now think about how this error is caught in terms of what happens in the execution context. This was caught during run time i.e. during the code execution phase when we were executing line
numSquare = num * num;. What we do here is that we calculatenum * numi.e.5 * 5which results in25and now try to assign it to the memory location labelled asnumSquarei.e. we try to assign the calculated value,25to the existing label,numSquare. Remember that memory is allocated during the memory creation phase for variables declared using var, let, or const. Since numSquare was never declared, no memory was allocated for it. Therefore, when JavaScript tries to assign a value to it during execution (in strict mode), it throws a ReferenceError.
How is JavaScript code executed ?
We have learned that JavaScript code is executed in an execution context. Understanding how JavaScript is executed behind the scenes is essentially same as understanding how the execution context is created through the 2 phases in detail. Let’s dive deep with an example.
var a = 4; //line 1
function multiply(x, y) { //line 2
var result = x * y; //line 3
return result; //line 4
} //line 5
var b = 5; //line 6
var output = multiply(a, b); //line 7
Let’s see what happens when the above code is run step by step.
1. Firstly a global execution context (GEC) is created and pushed into a stack (known as call stack) to keep track of the thread of execution i.e. to keep track of the line of control in the entire program.
Call stack
--->| GEC |
---------------------
2. Next, memory is created for the GEC, (phase 1 of GEC creation). What happens here is that the entire program is ‘parsed’ line by line to allocate space for all defined variables and function. At the end of this phase this is how the global memory would look like:
a: undefined
multiply: function multiply(x, y) {
var result = x * y;
return result;
}
b: undefined
output: undefined
Note: As mentioned earlier, undefined will be assigned initially to all variables and constants — a, b, output. Whereas the entire function code is assigned to the function — multiply.
3. Now, we enter the second phase of GEC creation — thread of execution or code execution phase. This is where the code starts executing starting from line 1.
4. Firstly, line 1 is executed. Here, 4 is assigned to the constant ‘a’. So, in the memory space we lookup allocated memory labelled as ‘a’ and assign 4 to it.
a: 4
multiply: f // the function's entire code is stored here
b: undefined
output: undefined
5. Now moving to line 2, which is a function definition (from line 2 to 5). Nothing to execute here (as it is not a function call but just a definition)
6. We have the function definition till line 5. So the next statement to be executed is in line 6, wherein we assign 5 to variable ‘b’. So, in the memory space we lookup allocated memory labelled as ‘b’ and assign 5 to it.
a: 4
multiply: f
b: 5
output: undefined
7. On line 7, we are calling the multiply function with the arguments a , b and then assign the result returned to the variable output. Here, the function call is first executed. So as we know, here a Function Execution Context (FEC) is created for the function ‘multiply’ and this is also pushed into call stack, with the point of control now pointing to line 2.
Call stack
--->| multiply(4,5)//FEC |
---------------------
| GEC |
---------------------
8. Local Memory for the FEC is created by parsing lines 2 to 5 (where function multiply is defined). At the end of this step, the memory of FEC looks like below:
x: undefined
y: undefined
result: undefined
Note here that, the parameters x and y are also allocated memory.
9. Now begins the thread of execution phase of the FEC i.e. the function’s code execution starting with assigning the arguments a and b that evaluates to 4 and 5 (from the global memory) to the parameters x and y respectively.
x: 4
y: 5
result: undefined
10. Next line 3 is executed where values of x and y are multiplied and assigned to result. So here, 4 * 5 is calculated and the product 20 is assigned to local variable result in the local space.
x: 4
y: 5
result: 20
11. Now point of control moves to the next line i.e. line 4 where the result is returned. JavaScript looks up result in the function’s local memory and returns it to the Global Execution Context. Two main things happen next. As soon as the return statement is executed the Function execution context is deleted. It is popped out from the call stack and now the control moves back to the GEC to the line where the function was originally invoked.
Call stack
| | ----------> multiply(4,5)//FEC is popped out
--->| GEC |
---------------------
12. The control is now back at line 7 where the function has returned the result 20 and this is now assigned to the global variable ‘output’.
a: 4
multiply: function multiply(x, y) {
var result = x * y;
return result;
}
b: 5
output: 20
13. Now the program is fully executed. Therefore the GEC is deleted i.e. it pops out from the call stack and the JavaScript code execution is complete. (Finally…)
Call stack
| | ----------> GEC is popped out
----------------------
Understanding execution context is the foundation for mastering hoisting, closures, scope, and asynchronous JavaScript. Once this mental model clicks, everything else becomes easier.
Happy coding. Cheers!
Top comments (0)