18 min read
An execution context is what the JavaScript engine uses to track the runtime evaluation of code.
Having a solid understanding of execution context in JavaScript is essential in order to understand other fundamental concepts developers often find confusing such as scope, closures and hoisting. Although execution context is not a feature you can access directly within your code, it is a concept defined by the ECMAScript specification detailing how the engine evaluates and executes your programs.
How the JavaScript engine executes code
The first thing to understand is the JavaScript engine executes code in two phases:
- Compilation phase
- Execution phase
Compilation Phase
Compilation consists of a few different phases. Before our code can be executed it needs to be broken up into tokens, this is typically referred to as tokenizing/lexing. The parser takes in this array of tokens and creates an abstract syntax tree or AST which will later be consumed during code generation, which is the process of transforming the AST into executable code.
The following example demonstrates JavaScript performs a separate compilation phase before execution. Because a SyntaxError is detected during compilation, the code never reaches the execution phase, which is why console.log(a) never runs.
let a = 'a'
console.log(a) // this never runs
a = .'a' // Uncaught SyntaxError: Unexpected token '.'Notice how 'a' is never logged to the console. The only way the JavaScript engine could know about this error is if the entire program was parsed first. If it was ran from the top down, the console.log(a) would run, then an error would be thrown.
The same thing happens below even though the function is never invoked.
const a = 'a'
console.log(a)
function foo() {
let b = 'b'
console.log(b)
b = .'b' // Uncaught SyntaxError: Unexpected token '.'
}Here is another interesting example, let is declared twice in the same scope. Even though the function is never invoked a SyntaxError is thrown.
function foo() {
let a = 1
let a = 2
}
// Uncaught SyntaxError: Identifier 'a' has already been declaredLexical Analysis (Lexing/Scanning)
The first step in processing JavaScript code is lexical analysis. This involves reading the raw source code character by character and grouping them into meaningful units called tokens. These tokens represent the basic building blocks of the language, such as if, while, const, let, +, =, {}, () ect.
The ECMAScript specification, which defines JavaScript, includes a detailed "Lexical Grammar" section. This grammar precisely defines how characters are grouped into tokens, including rules for identifiers, keywords, literals, and how whitespace and line terminators are handled.
The lexer is what implements the lexical grammar.
We can see below in the code block there is a simple variable declaration, but in order for this program to be executed it first needs to be tokenized.
const x = 1This code will be broken up into tokens const, x, =, and 1.
Syntax Analysis (Parsing)
The JavaScript engine feeds the token stream to the parser, which constructs the AST - a hierarchical representation of the program’s syntactic structure. During this step, the parser enforces JavaScript’s syntactic grammar. If a violation is found, compilation stops with a SyntaxError.
The actual resolution of scopes and identifier bindings is performed later during the semantic analysis phase.
Semantic Analysis
During semantic analysis, the engine traverses the AST to construct and consult a separate scope structure representing lexical environments.
This scope structure is used to create bindings, resolve identifier references to the correct bindings (including shadowing), and detect early errors such as invalid or duplicate declarations within the same scope.
At this stage, scopes and bindings exist only as part of the program’s static structure. Memory for variables is allocated later during execution, when the runtime creates environment records for each scope according to the structure determined during semantic analysis.
In the code below, the analyzer associates bindings with their correct lexical scopes. const x = 1 belongs to the function scope, while const x = 2 belongs to the inner block scope created by the if statement. If two const x declarations appeared in the same block, the analyzer would report a SyntaxError during static semantic analysis.
function foo() {
const x = 1
if (x > 0) {
const x = 2
console.log(x)
}
}Execution Phase
Once the JavaScript engine has parsed the source code and generated the executable code during the compilation phase, it proceeds to the execution phase, where that code is evaluated within execution contexts.
Execution Context
An execution context is created whenever a function is invoked or code is evaluated in global scope.
Execution contexts are tracked using the execution context stack, commonly called the callstack. Each entry in the callstack represents an execution context, and the topmost entry is referred to by the specification as the running execution context. At any point during execution, the running execution context may be suspended and a different execution context will become the running execution context.
After the execution context is created and pushed onto the execution context stack, the engine performs declaration instantiation, which creates and initializes bindings before any statements execute. During this step, function declaration bindings are initialized to their function objects, var bindings are initialized to undefined, and let and const bindings are created but remain uninitialized until their declarations are evaluated.
Once declaration instantiation completes, the engine evaluates the statement list associated with that execution context.
Although many explanations refer to a “creation phase” and an “execution phase,” the specification models execution more precisely as:
- creating and pushing an execution context onto the execution context stack
- performing declaration instantiation
- evaluating the statement list
function foo() {
debugger
// 1. foo is the running execution context
function bar() {
// 3. foo is suspended and bar is the running execution context
debugger
console.log('bar')
}
bar() // 2. a new execution context is pushed onto the callstack
// 4. bar is popped off the callstack and foo is the running execution context
console.log('foo')
}
foo()Types of Execution Context
There are different types of execution contexts, but they all serve the same purpose: tracking the runtime evaluation of code.
LexicalEnvironment and VariableEnvironment state components.Global Execution Context
Any time code is evaluated, there is always a global execution context, such as when executing a for loop in global scope.
The global execution context is created when a script is evaluated as part of ScriptEvaluation and represents execution of code at the top level. There is at most one global execution context per script, and in DevTools it appears in the call stack as (anonymous).
Although the global execution context is often credited with setting up the global environment, that work is performed earlier by the host during realm initialization (InitializeHostDefinedRealm), before ScriptEvaluation creates the global execution context.
Before any global execution context exists, the host creates an original execution context to serve as the running execution context while the realm, global object, and global environment are initialized (InitializeHostDefinedRealm). This initialization is internal, and the original execution context is not visible in the DevTools call stack; only the global execution context appears there as (anonymous).
Function Execution Context
A function execution context is created whenever a function is invoked. Each function invocation gets its own execution context, which is pushed onto the execution context stack when the function begins execution and popped off the stack when the function returns. Function execution contexts are created as part of the PrepareForOrdinaryCall operation.
Execution Context Components
Each execution context contains certain state components which are pointers to environment records. Two of these state components are the LexicalEnvironment and the VariableEnvironment.
Lexical Environment
The LexicalEnvironment state component of the execution context identifies the environment record used to resolve identifier references made by code within this execution context.
An environment record defines the association between identifiers and their bindings according to the lexical nesting structure of the source code. Each environment record has an [[OuterEnv]] field which is null for the global scope or a reference to an outer environment record.
Variable Environment
The VariableEnvironment state component of the execution context is a pointer to the Environment Record that holds bindings created by var declarations within this execution context.
let and const belong to the LexicalEnvironment, not the VariableEnvironment.Environment Records
An environment record is the runtime representation of a scope. Lexical scope is determined at compile time during semantic analysis, environment records are created at runtime based on that structure.
Whenever a declaration is evaluated, such as const, let, or function, the engine creates a binding for the declared identifier in an appropriate environment record. Which type of environment record is used depends on where the declaration appears (global, function, block, or module) and on the kind of declaration.
A binding (sometimes called an identifier binding) associates an identifier name with a storage location managed by the JavaScript engine. These bindings are stored in environment records and are used during identifier resolution to determine the binding associated with a given identifier in order to access or update the value stored in that location.
In the code below, someLetter is an identifier. When execution begins, a binding for someLetter is created in the appropriate environment record with an uninitialized value. When code execution reaches the declaration, the binding’s value is initialized to 'a'.
A variable is an informal term for an identifier that has a binding in an environment record. A binding is the association between that identifier and a storage location, which holds the identifier’s value.
const someLetter = 'a'
var someNumber = 1
function foo() {
console.log('foo')
}Here is a small and simple pseudocode example to help visualize this concept.
GlobalExecutionContext = {
LexicalEnvironment: GlobalEnvironmentRecord,
VariableEnvironment: GlobalEnvironmentRecord
}
GlobalEnvironmentRecord = {
Type: GlobalEnvironmentRecord,
[[ObjectRecord]]: {
Type: ObjectEnvironmentRecord,
[[BindingObject]]: <global object>,
Bindings: {
someNumber: { [[Value]]: 1 },
foo: { [[Value]]: <foo function object> }
},
[[OuterEnv]]: null
},
[[DeclarativeRecord]]: {
Type: DeclarativeEnvironmentRecord,
Bindings: {
someLetter: { [[Value]]: 'a' },
},
[[OuterEnv]]: null
},
[[GlobalThisValue]]: <global object>,
[[OuterEnv]]: null
}For Loops do not create a new execution context
Evaluating a for loop does not create a new execution context. For for statements with lexical declarations, a new environment record is created for each iteration, and identifier bindings are created in that environment record. During each iteration, the running execution context’s LexicalEnvironment is set to the newly created environment record, resulting in new identifier bindings for let and const on each iteration.
When evaluation of the for loop completes, the running execution context’s LexicalEnvironment is set to the environment record before evaluation of the for loop began.
When a for loop appears in global scope, it is evaluated within the global execution context. No additional execution contexts are created, only the global execution context’s LexicalEnvironment is set to the new environment record created for each iteration.
.forEach() method executes a provided function once for each array element. Therefore, for each callback invocation a new execution context is created along with a new environment record for that execution context.Blocks do not create a new execution context
When a block is evaluated, a new environment record is created, and identifier bindings are created in that environment record. The running execution context's lexical environment is set to this newly created environment record. Once evaluation for the block is complete, the running execution context's lexical environment is set to the previous environment record before evaluation of the block began.
function foo() {
const x = 1
if (x) {
// a new environment record is created for this block
const y = 2
}
{
// a new environment record is also created here
const x = 2
}
}Code Execution Example
At this point, the compilation phase has finished and executable code has been generated. The JavaScript engine will create an execution context for this block of code.
const a = 1
let b = 2
var c = 3
function foo() {
const a = 1
if (a > 0) {
const a = 3
return a + b + c
}
}
foo()The JavaScript engine creates a new execution context along with a new environment record and pushes it onto the execution context stack. It then performs GlobalDeclarationInstantiation, which creates and initializes bindings in that environment record. Function declarations are initialized to function objects, var bindings are initialized to undefined, and let and const bindings are created but remain uninitialized until their declarations are evaluated.
GlobalExecutionContext = {
LexicalEnvironment: GlobalEnvironmentRecord,
VariableEnvironment: GlobalEnvironmentRecord
}
GlobalEnvironmentRecord = {
Type: GlobalEnvironmentRecord,
[[ObjectRecord]]: {
Type: ObjectEnvironmentRecord,
[[BindingObject]]: <global object>, // e.g., window in browsers
Bindings: {
// Built-in globals (e.g., Math, Array, JSON, etc.)
c: { [[Value]]: undefined },
foo: { [[Value]]: <foo function object>
},
[[OuterEnv]]: null
},
[[DeclarativeRecord]]: {
Type: DeclarativeEnvironmentRecord,
Bindings: {
a: { [[Value]]: uninitialized },
b: { [[Value]]: uninitialized }
},
[[OuterEnv]]: null
},
[[GlobalThisValue]]: <global object>,
[[OuterEnv]]: null
}After the bindings have been initialized from GlobalDeclarationInstantiation, the engine evaluates the statement list associated with that execution context.
const a = 1
let b = 2
var c = 3
function foo() {
const a = 1
if (a > 0) {
const a = 3
return a + b + c
}
}
foo()GlobalExecutionContext = {
LexicalEnvironment: GlobalEnvironmentRecord,
VariableEnvironment: GlobalEnvironmentRecord
}
GlobalEnvironmentRecord = {
Type: GlobalEnvironmentRecord,
[[ObjectRecord]]: {
Type: ObjectEnvironmentRecord,
[[BindingObject]]: <global object>, // e.g., window in browsers
Bindings: {
// Built-in globals (e.g., Math, Array, JSON, etc.)
c: { [[Value]]: undefined },
foo: { [[Value]]: <foo function object>
},
[[OuterEnv]]: null
},
[[DeclarativeRecord]]: {
Type: DeclarativeEnvironmentRecord,
Bindings: {
a: { [[Value]]: 1 },
b: { [[Value]]: uninitialized },
},
[[OuterEnv]]: null
},
[[GlobalThisValue]]: <global object>,
[[OuterEnv]]: null
}const a = 1
let b = 2
var c = 3
function foo() {
const a = 1
if (a > 0) {
const a = 3
return a + b + c
}
}
foo()GlobalExecutionContext = {
LexicalEnvironment: GlobalEnvironmentRecord,
VariableEnvironment: GlobalEnvironmentRecord
}
GlobalEnvironmentRecord = {
Type: GlobalEnvironmentRecord,
[[ObjectRecord]]: {
Type: ObjectEnvironmentRecord,
[[BindingObject]]: <global object>, // e.g., window in browsers
Bindings: {
// Built-in globals (e.g., Math, Array, JSON, etc.)
c: { [[Value]]: undefined },
foo: { [[Value]]: <foo function object>
},
[[OuterEnv]]: null
},
[[DeclarativeRecord]]: {
Type: DeclarativeEnvironmentRecord,
Bindings: {
a: { [[Value]]: 1 },
b: { [[Value]]: 2 },
},
[[OuterEnv]]: null
},
[[GlobalThisValue]]: <global object>,
[[OuterEnv]]: null
}const a = 1
let b = 2
var c = 3
function foo() {
const a = 1
if (a > 0) {
const a = 3
return a + b + c
}
}
foo()GlobalExecutionContext = {
LexicalEnvironment: GlobalEnvironmentRecord,
VariableEnvironment: GlobalEnvironmentRecord
}
GlobalEnvironmentRecord = {
Type: GlobalEnvironmentRecord,
[[ObjectRecord]]: {
Type: ObjectEnvironmentRecord,
[[BindingObject]]: <global object>, // e.g., window in browsers
Bindings: {
// Built-in globals (e.g., Math, Array, JSON, etc.)
c: { [[Value]]: 3 },
foo: { [[Value]]: <foo function object>
},
[[OuterEnv]]: null
},
[[DeclarativeRecord]]: {
Type: DeclarativeEnvironmentRecord,
Bindings: {
a: { [[Value]]: 1 },
b: { [[Value]]: 2 },
},
[[OuterEnv]]: null
},
[[GlobalThisValue]]: <global object>,
[[OuterEnv]]: null
}There is nothing to do here since foo is already initialized.
const a = 1
let b = 2
var c = 3
function foo() {
const a = 1
if (a > 0) {
const a = 3
return a + b + c
}
}
foo()Evaluation reaches a function invocation and the foo function object’s internal [[Call]] method is invoked. A new function execution context is created with a new function environment record and pushed onto the execution context stack. FunctionDeclarationInstantiation is then performed, creating and initializing bindings in the function environment record.
const a = 1
let b = 2
var c = 3
function foo() {
const a = 1
if (a > 0) {
const a = 3
return a + b + c
}
}
foo()FooExecutionContext = {
LexicalEnvironment: FooEnvironmentRecord,
VariableEnvironment: FooEnvironmentRecord
}
FooEnvironmentRecord = {
type: FunctionEnvironmentRecord,
Bindings: {
a: { [[Value]]: uninitialized }
},
[[ThisValue]]: window, // for browsers
[[ThisBindingStatus]]: "initialized",
[[FunctionObject]]: <reference to foo>,
[[NewTarget]]: undefined,
[[OuterEnv]]: GlobalEnvironmentRecord
}
GlobalExecutionContext = {
LexicalEnvironment: GlobalEnvironmentRecord,
VariableEnvironment: GlobalEnvironmentRecord
}
GlobalEnvironmentRecord = {
Type: GlobalEnvironmentRecord,
[[ObjectRecord]]: {
Type: ObjectEnvironmentRecord,
[[BindingObject]]: <global object>, // e.g., window in browsers
Bindings: {
// Built-in globals (e.g., Math, Array, JSON, etc.)
c: { [[Value]]: 3 },
foo: { [[Value]]: <foo function object>
},
[[OuterEnv]]: null
},
[[DeclarativeRecord]]: {
Type: DeclarativeEnvironmentRecord,
Bindings: {
a: { [[Value]]: 1 },
b: { [[Value]]: 2 },
},
[[OuterEnv]]: null
},
[[GlobalThisValue]]: <global object>,
[[OuterEnv]]: null
}After the bindings have been initialized from FunctionDeclarationInstantiation, the engine evaluates the statement list associated with that function execution context.
const a = 1
let b = 2
var c = 3
function foo() {
const a = 1
if (a > 0) {
const a = 3
return a + b + c
}
}
foo()FooExecutionContext = {
LexicalEnvironment: FooEnvironmentRecord,
VariableEnvironment: FooEnvironmentRecord
}
FooEnvironmentRecord = {
type: FunctionEnvironmentRecord,
Bindings: {
a: { [[Value]]: 1 }
},
[[ThisValue]]: window, // for browsers
[[ThisBindingStatus]]: "initialized",
[[FunctionObject]]: <reference to foo>,
[[NewTarget]]: undefined,
[[OuterEnv]]: GlobalEnvironmentRecord
}
GlobalExecutionContext = {
LexicalEnvironment: GlobalEnvironmentRecord,
VariableEnvironment: GlobalEnvironmentRecord
}
GlobalEnvironmentRecord = {
Type: GlobalEnvironmentRecord,
[[ObjectRecord]]: {
Type: ObjectEnvironmentRecord,
[[BindingObject]]: <global object>, // e.g., window in browsers
Bindings: {
// Built-in globals (e.g., Math, Array, JSON, etc.)
c: { [[Value]]: 3 },
foo: { [[Value]]: <foo function object>
},
[[OuterEnv]]: null
},
[[DeclarativeRecord]]: {
Type: DeclarativeEnvironmentRecord,
Bindings: {
a: { [[Value]]: 1 },
b: { [[Value]]: 2 },
},
[[OuterEnv]]: null
},
[[GlobalThisValue]]: <global object>,
[[OuterEnv]]: null
}const a = 1
let b = 2
var c = 3
function foo() {
const a = 1
if (a > 0) {
const a = 3
return a + b + c
}
}
foo()The if (a > 0) condition evaluates to true and the code inside the block is evaluated.
FooExecutionContext = {
LexicalEnvironment: FooBlockEnvironmentRecord,
VariableEnvironment: FooEnvironmentRecord
}
FooBlockEnvironmentRecord = {
type: DeclarativeEnvironmentRecord,
Bindings: {
a: { [[Value]]: uninitialized }
},
[[OuterEnv]]: FooEnvironmentRecord
}
FooEnvironmentRecord = {
type: FunctionEnvironmentRecord,
Bindings: {
a: { [[Value]]: 1 }
},
[[ThisValue]]: window, // for browsers
[[ThisBindingStatus]]: "initialized",
[[FunctionObject]]: <reference to foo>,
[[NewTarget]]: undefined,
[[OuterEnv]]: GlobalEnvironmentRecord
}
GlobalExecutionContext = {
LexicalEnvironment: GlobalEnvironmentRecord,
VariableEnvironment: GlobalEnvironmentRecord
}
GlobalEnvironmentRecord = {
Type: GlobalEnvironmentRecord,
[[ObjectRecord]]: {
Type: ObjectEnvironmentRecord,
[[BindingObject]]: <global object>, // e.g., window in browsers
Bindings: {
// Built-in globals (e.g., Math, Array, JSON, etc.)
c: { [[Value]]: 3 },
foo: { [[Value]]: <foo function object>
},
[[OuterEnv]]: null
},
[[DeclarativeRecord]]: {
Type: DeclarativeEnvironmentRecord,
Bindings: {
a: { [[Value]]: 1 },
b: { [[Value]]: 2 },
},
[[OuterEnv]]: null
},
[[GlobalThisValue]]: <global object>,
[[OuterEnv]]: null
}const a = 1
let b = 2
var c = 3
function foo() {
const a = 1
if (a > 0) {
const a = 3
return a + b + c
}
}
foo()FooExecutionContext = {
LexicalEnvironment: FooBlockEnvironmentRecord,
VariableEnvironment: FooEnvironmentRecord
}
FooBlockEnvironmentRecord = {
type: DeclarativeEnvironmentRecord,
Bindings: {
a: { [[Value]]: 3 }
},
[[OuterEnv]]: FooEnvironmentRecord
}
FooEnvironmentRecord = {
type: FunctionEnvironmentRecord,
Bindings: {
a: { [[Value]]: 1 }
},
[[ThisValue]]: window, // for browsers
[[ThisBindingStatus]]: "initialized",
[[FunctionObject]]: <reference to foo>,
[[NewTarget]]: undefined,
[[OuterEnv]]: GlobalEnvironmentRecord
}
GlobalExecutionContext = {
LexicalEnvironment: GlobalEnvironmentRecord,
VariableEnvironment: GlobalEnvironmentRecord
}
GlobalEnvironmentRecord = {
Type: GlobalEnvironmentRecord,
[[ObjectRecord]]: {
Type: ObjectEnvironmentRecord,
[[BindingObject]]: <global object>, // e.g., window in browsers
Bindings: {
// Built-in globals (e.g., Math, Array, JSON, etc.)
c: { [[Value]]: 3 },
foo: { [[Value]]: <foo function object>
},
[[OuterEnv]]: null
},
[[DeclarativeRecord]]: {
Type: DeclarativeEnvironmentRecord,
Bindings: {
a: { [[Value]]: 1 },
b: { [[Value]]: 2 },
},
[[OuterEnv]]: null
},
[[GlobalThisValue]]: <global object>,
[[OuterEnv]]: null
}The return statement causes the function to cease evaluation and the value 8 is returned. The foo execution context is removed from the execution context stack and the global execution context is the running execution context.
const a = 1
let b = 2
var c = 3
function foo() {
const a = 1
if (a > 0) {
const a = 3
return a + b + c
}
}
foo()GlobalExecutionContext = {
LexicalEnvironment: GlobalEnvironmentRecord,
VariableEnvironment: GlobalEnvironmentRecord
}
GlobalEnvironmentRecord = {
Type: GlobalEnvironmentRecord,
[[ObjectRecord]]: {
Type: ObjectEnvironmentRecord,
[[BindingObject]]: <global object>, // e.g., window in browsers
Bindings: {
// Built-in globals (e.g., Math, Array, JSON, etc.)
c: { [[Value]]: 3 },
foo: { [[Value]]: <foo function object>
},
[[OuterEnv]]: null
},
[[DeclarativeRecord]]: {
Type: DeclarativeEnvironmentRecord,
Bindings: {
a: { [[Value]]: 1 },
b: { [[Value]]: 2 },
},
[[OuterEnv]]: null
},
[[GlobalThisValue]]: <global object>,
[[OuterEnv]]: null
}const a = 1
let b = 2
var c = 3
function foo() {
const a = 1
if (a > 0) {
const a = 3
return a + b + c
}
}
foo()There is nothing left for the global execution context to evaluate in the script so the global execution context is removed from the execution context stack and code execution is complete.
Each time foo is invoked, the entire process repeats. A new execution context and its environment record are instantiated, and identifier bindings are created in that environment record. When the execution context is removed from the execution context stack, its environment record becomes unreachable and is eligible for garbage collection, and all the bindings associated with that environment record cease to exist, except in the case of closures.
Conclusion
Execution contexts are a foundational concept and are key to understanding scope, hoisting, the temporal dead zone, and the this keyword. Before any code is executed, the JavaScript engine performs a compilation step where the lexical structure of the code is analyzed and scopes are identified during semantic analysis. JavaScript is lexically scoped, meaning the physical structure of the code determines variable and function visibility. When execution begins, compiled code is evaluated within execution contexts, and the scopes identified during compilation are instantiated as environment records.