Kazi Rahamatullah
Kazi Rahamatullah
AboutProjectsBlogContact
UsesBooks
ResumeView CV
Kazi Rahamatullah

© Copyright 2026 Kazi Rahamatullah

AboutProjectsBlogBooksUses
Twitter/XGitHubProduct HuntCodeSandbox
Back to Blog
JavaScriptExecution ContextCall StackInterview PreparationFrontend Development

JavaScript Execution Context

Deep dive into how JavaScript runs your code — execution context, creation and execution phases, the call stack, lexical environment, GEC vs FEC, and stack vs heap memory.

Jul 3, 202614 min read
JavaScript Execution Context

Introduction

Every line of JavaScript you write eventually runs inside an execution context — the environment the engine creates to manage variables, this, scope, and the order in which code executes. Understanding execution context is not optional trivia; it is the foundation behind scope, closures, hoisting, and the this keyword.

This guide walks from source code to machine execution:

  1. What execution context is and why it matters
  2. How the engine processes code before it runs
  3. Lexical environment vs execution context
  4. Global and function execution contexts
  5. Creation and execution phases
  6. The call stack
  7. Stack vs heap memory

Each section includes runnable examples you can paste into the browser console or Node.js.

Course Reference

Quick index

#Section
1What is execution context?
2Why it matters
3How the engine processes code
4Lexical environment vs context
5Global execution context (GEC)
6Creation and execution phases
↳ Creation vs execution demo
7Function execution context (FEC)
8The call stack
↳ Call stack playground
9Stack vs heap memory
↳ Stack vs heap visualizer

1. What is execution context?

An execution context is the environment in which JavaScript code is evaluated and executed. Think of it as a container the engine builds for a specific chunk of code — global script, a function body, or an eval block.

Every context has three essential components:

ComponentRole
Variable environmentStores var declarations, function declarations, and let/const bindings
Lexical environmentResolves identifiers through scope chains
this bindingDetermines what this refers to inside the current code

When JavaScript starts running a file, it does not jump straight into line-by-line execution. It first creates a context, sets up memory for variables and functions, then runs the code inside that context.

javascript
const greeting = 'Hello'
 
function sayHello(name) {
  const message = `${greeting}, ${name}!`
  console.log(message)
}
 
sayHello('Kazi')
// "Hello, Kazi!"

Running this script involves at least two contexts: one for the global script and one created each time sayHello is invoked.

Note

Execution context is the runtime management layer. It answers: where am I running, what variables exist here, and what does this mean?

Back to index


2. Why it matters

Execution context is the invisible infrastructure behind several concepts you use every day.

Scope and closures

Scope is not a static label on a block of code — it is determined by the lexical environment attached to an execution context. Closures exist because a function retains access to the lexical environment from the context where it was created, even after that outer context has finished executing.

javascript
function createCounter() {
  let count = 0
  return function () {
    count++
    return count
  }
}
 
const counter = createCounter()
console.log(counter()) // 1
console.log(counter()) // 2

The inner function closes over the variable environment of createCounter's execution context.

The this keyword

this is not resolved by where a function is written — it is bound when the function's execution context is created. That binding depends on how the function is called (global, method, constructor, call/apply/bind, arrow function).

javascript
const user = {
  name: 'Kazi',
  greet() {
    console.log(this.name)
  },
}
 
user.greet() // 'Kazi' — `this` is `user`
 
const fn = user.greet
fn() // undefined in strict mode — `this` is not `user`

Hoisting and temporal dead zone

During the creation phase, the engine allocates memory for declarations before any line runs. That is why function declarations appear available before their definition line, and why let/const exist in a temporal dead zone until their declaration line executes.

Tip

When debugging "why is this variable undefined?" or "why does this behave differently here?", trace the execution context first — not just the source code layout.

Back to index


3. How the engine processes code

Before JavaScript executes anything, the engine transforms your source code through several stages.

plaintext
Source Code → Tokenization → AST → Compilation → Machine Code → Execution
StageWhat happens
Source codeThe .js file or <script> you write
TokenizationCode is broken into tokens (const, =, 'hello', {, })
ASTTokens form an Abstract Syntax Tree — a structured representation
CompilationModern engines (V8, SpiderMonkey) use JIT — compile hot paths to machine code
ExecutionMachine code runs inside execution contexts on the call stack
javascript
// What you write
function add(a, b) {
  return a + b
}
 
add(2, 3)

The engine does not execute this character by character. It parses the entire function into an AST, compiles it, then creates an execution context when add is called.

Performance

Just-in-time (JIT) compilation means frequently executed functions get optimized machine code. That is one reason micro-benchmarks can mislead — the engine may optimize differently depending on call patterns and types.

Back to index


4. Lexical environment vs execution context

These two terms are related but not interchangeable.

TermFocus
Lexical environmentWhere identifiers are resolved — scope chain, outer references
Execution contextHow the current code runs — phases, this, stack management

A lexical environment is a record with:

  • An environment record (variable bindings)
  • A reference to an outer lexical environment (parent scope)

An execution context contains a lexical environment plus the this binding and other runtime metadata.

javascript
const outer = 'global'
 
function outerFn() {
  const middle = 'middle'
 
  function innerFn() {
    const inner = 'inner'
    console.log(inner, middle, outer)
  }
 
  innerFn()
}
 
outerFn()
// 'inner' 'middle' 'global'

When innerFn runs, its lexical environment links outward: innerFn → outerFn → global. The execution context for innerFn uses that chain to resolve middle and outer.

Note

Lexical means determined by source code placement — where { and } sit in the file. The scope chain is fixed at write time; execution context is created at run time.

Interview Answer

The lexical environment handles identifier lookup through the scope chain. The execution context is the broader runtime wrapper that includes the lexical environment, this binding, and lifecycle (creation → execution → pop from stack). Every execution context has a lexical environment, but not every lexical environment is a full execution context on its own.

Back to index


5. Global execution context (GEC)

The Global Execution Context is created automatically when a JavaScript program starts. There is exactly one GEC per runtime (per tab in the browser, per Node.js process).

EnvironmentGlobal objectthis in global scope
Browserwindowwindow (non-strict)
Node.jsglobalglobal (non-strict)
ModulesModule scope (not window)undefined in ES modules
javascript
// Browser (non-module script)
var title = 'Blog'
console.log(window.title) // 'Blog'
 
function showGlobal() {
  console.log(this === window) // true in browser, non-strict
}
showGlobal()

In ES modules (type="module"), top-level variables are scoped to the module — they do not become properties of window.

javascript
// ES module
const apiUrl = 'https://api.example.com'
// window.apiUrl → undefined

Warning

Relying on var at the global level pollutes window in browsers. Prefer const/let and modules to keep the global namespace clean.

Back to index


6. Creation and execution phases

Every execution context — global or function — goes through two phases.

Phase 1: Creation

Before any statement runs, the engine:

  1. Creates the variable and lexical environments
  2. Sets up the scope chain (outer reference)
  3. Determines this binding
  4. Allocates memory for declarations

During creation:

Declaration typeMemory behavior during creation phase
varAllocated and initialized to undefined
function declarationFully stored (hoisted with body)
let / constAllocated but not initialized (TDZ)
javascript
console.log(typeof hoistedFn) // 'function'
console.log(typeof hoistedVar) // 'undefined'
 
hoistedFn() // works — function body is available
// console.log(hoistedLet) // ReferenceError — TDZ
 
var hoistedVar
function hoistedFn() {
  return 'I was hoisted'
}
let hoistedLet = 'not yet'

Interactive: creation vs execution

Creation vs Execution Phase

Toggle phases and step through execution to see hoisting, TDZ, and when values become available.

source.js

1console.log(typeof hoistedFn) // 'function'
2console.log(typeof hoistedVar) // 'undefined'
3hoistedFn() // works
4// console.log(hoistedLet) // ReferenceError
5var hoistedVar
6function hoistedFn() { return "I was hoisted" }
7let hoistedLet = 'not yet'

Memory after creation — no user code has run yet

hoistedVarvar

undefined

hoistedFnfunction

ƒ hoistedFn() { return "I was hoisted" }

hoistedLetlet

⟨uninitialized⟩

Temporal Dead Zone — access throws ReferenceError

In creation, `function` declarations are fully hoisted; `var` is `undefined`; `let`/`const` exist but cannot be read until their line runs.

Phase 2: Execution

The engine runs code line by line:

  • Assigns actual values to variables
  • Executes expressions and function bodies
  • Creates new function execution contexts on calls
javascript
var score = 10 // creation: score = undefined → execution: score = 10
 
function double(n) {
  return n * 2
}
 
var result = double(score) // FEC created → runs → returns 20
console.log(result) // 20

Interview Answer

Creation phase: memory allocation, hoisting, this binding, scope chain setup — no user code runs yet. Execution phase: assignments, expressions, function invocations run in order. Hoisting is a creation-phase behavior: var and function declarations are registered before the first line executes.

Back to index


7. Function execution context (FEC)

A Function Execution Context is created every time a function is invoked — not when it is defined.

ContextWhen createdHow many exist
GECProgram startOne per runtime
FECFunction callOne per active invocation
javascript
function multiply(a, b) {
  const product = a * b
  return product
}
 
multiply(3, 4) // FEC #1 — arguments: a=3, b=4
multiply(5, 2) // FEC #2 — separate context, separate `product`

Each FEC gets:

  • Its own argument object / parameter bindings
  • A local variable environment
  • Its own this binding (unless arrow function — inherits from enclosing lexical this)
  • A link to the outer lexical environment (closure)
javascript
function createGreeter(prefix) {
  return function (name) {
    // FEC for this inner function links to createGreeter's environment
    return `${prefix}, ${name}!`
  }
}
 
const sayHi = createGreeter('Hi')
console.log(sayHi('Kazi')) // 'Hi, Kazi!'

Even after createGreeter returns and its FEC is popped off the stack, the inner function keeps a reference to prefix through the closure.

Tip

GEC is created once and lives for the program lifetime. FECs are created and destroyed on every call — that is why recursion depth is limited by the call stack size.

Back to index


8. The call stack

The call stack (execution context stack) is a LIFO structure that tracks which context is currently running.

plaintext
┌─────────────────┐
│  innerFn() FEC  │  ← top (currently executing)
├─────────────────┤
│  outerFn() FEC  │
├─────────────────┤
│  GEC            │  ← bottom
└─────────────────┘

Rules:

  • Push a new context when a function is called
  • Pop a context when a function returns
  • Only the top context executes at any moment
javascript
function a() {
  console.log('a start')
  b()
  console.log('a end')
}
 
function b() {
  console.log('b start')
  c()
  console.log('b end')
}
 
function c() {
  console.log('c')
}
 
a()

Output:

plaintext
a start
b start
c
b end
a end

Stack timeline:

  1. GEC runs → calls a() → push FEC for a
  2. a calls b() → push FEC for b
  3. b calls c() → push FEC for c
  4. c returns → pop c
  5. b returns → pop b
  6. a returns → pop a

Interactive: call stack playground

Call Stack Playground

Step through nested function calls and watch contexts push onto and pop off the LIFO stack.

Step 1 / 12

Program starts — Global Execution Context (GEC) is created and pushed

nested-calls.js

1function a() {
2 console.log('a start')
3 b()
4 console.log('a end')
5}
6function b() {
7 console.log('b start')
8 c()
9 console.log('b end')
10}
11function c() {
12 console.log('c')
13}
14a()

Call stack (top → bottom)

GEC (global)gec

← currently executing

↑ push on call · pop on return ↓

Console output

—

Only the frame at the top of the stack runs code. When a function returns, its context is popped and control returns to the frame below.

Interview Answer

The call stack is a LIFO (Last-In, First-Out) structure. Each function call pushes a new execution context; each return pops it. Synchronous JavaScript is single-threaded — one call stack, one thing executing at a time. A stack overflow happens when calls exceed the engine's stack limit (deep recursion without a base case).

Warning

Deep synchronous recursion can throw RangeError: Maximum call stack size exceeded. For large iterative workloads, prefer loops or async patterns that do not grow the stack unbounded.

Back to index


9. Stack vs heap memory

JavaScript uses two memory regions. Execution contexts and the call stack interact with both.

MemoryStoresAccess speed
StackPrimitives, context frames, reference addressesFast, fixed size per frame
HeapObjects, arrays, functions, closures (referenced data)Larger, managed by GC
javascript
// Stack: primitive values and references
let count = 42 // number on stack
let active = true // boolean on stack
let ref = { id: 1 } // ref (address) on stack → object on heap
 
// Heap: the object `{ id: 1 }` lives here
ref.name = 'Kazi' // mutates heap object
javascript
function makeUser(name) {
  return { name, createdAt: Date.now() } // new heap object each call
}
 
const a = makeUser('Alice')
const b = makeUser('Bob')
// `a` and `b` are different heap objects; stack holds two separate references

Interactive: stack vs heap

Stack vs Heap Visualizer

Switch scenarios to see where primitives, references, and shared objects live in memory.

let count = 42

let active = true

let label = 'JS'

Stack

count42
activetrue
label"JS"

Fast, fixed-size frames. Freed when context pops.

Heap

No heap allocations — all values are primitives on the stack.

Objects and functions. Reclaimed by garbage collection when unreachable.

Primitive values are stored directly on the stack — copied by value.

Performance

Primitives copied by value stay on the stack. Objects are copied by reference — assigning const b = a copies the pointer, not the heap object. Large object churn increases garbage collection pressure; reuse or pool objects in hot paths when profiling shows GC bottlenecks.

Garbage collection

When no reference points to a heap object anymore, the engine's garbage collector reclaims that memory. Closures keep outer variables alive on the heap as long as the inner function is reachable.

javascript
function leakExample() {
  const huge = new Array(1_000_000).fill('data')
  return function () {
    return huge.length // `huge` stays on heap while this function exists
  }
}
 
const getter = leakExample()
// `huge` cannot be collected until `getter` is unreachable

Interview Answer

The stack holds execution context frames and primitive values — fast, automatically freed when a function returns. The heap stores objects and functions — slower to allocate, freed by garbage collection when no references remain. Closures keep outer scope variables on the heap alive beyond their execution context's lifetime.

Back to index


Conclusion

JavaScript execution context is the bridge between the code you write and how the engine actually runs it. Every script starts in a global context; every function call spins up a new one; the call stack keeps synchronous execution ordered; and lexical environments power scope and closures.

Key takeaways:

  • Execution context = variable environment + lexical environment + this binding
  • GEC is created once at startup; FEC is created on every function call
  • Creation phase hoists declarations; execution phase assigns values and runs logic
  • The call stack is LIFO — push on call, pop on return
  • Stack holds primitives and frames; heap holds objects — closures keep heap data alive

The next time hoisting, closures, or this confuse you, ask: which execution context am I in, and what does its lexical environment look like? That question usually leads straight to the answer.


Have questions about execution context or closures? Reach out via the contact page.

Share this article

XLinkedInFacebook
Kazi Rahamatullah

Written by

Kazi Rahamatullah

FullStack Developer

X / TwitterGitHubLinkedIn

Subscribe to my newsletter

Stay up to date and get notified when I share new contents.

No spam ever, unsubscribe anytime