Demystifying JavaScript Execution Context, Scope Chain & Hoisting

TL;DR:
Every time JavaScript runs your code, it creates an execution context (global, function, or eval) that holds a lexical environment. Identifiers are looked up the scope chainβfrom innermost to outermost. During the compile phase, declarations are hoisted: var
and function declarations move to the top (initialized to undefined
for var
), while let
/const
enter a Temporal Dead Zone until their declaration is reached. Knowing this prevents unexpected undefined
, reference errors, and scope leaks.
Why This Matters
Misunderstanding execution context, scope chain, or hoisting leads to:
- Surprising
undefined
values or reference errors - Accidental globals when
var
or undeclared variables slip through - Closure bugs when inner functions capture the wrong environment
- Hard-to-read code, especially with mixed
var
,let
, andconst
Mastering these concepts helps you write predictable, maintainable, and bug-resistant JavaScript.
1. Execution Context
An execution context is the environment in which JavaScript code is evaluated and executed. There are three types:
- Global Execution Context
- Created when your script first runs
- Creates the global object (
window
in browsers,global
in Node) andthis
binding
- Function Execution Context
- Created on each function invocation
- Contains its own Variable Environment, Lexical Environment, and
this
- Eval Execution Context
- Created by calls to
eval()
(avoid using it!)
- Created by calls to
Each function context goes through two phases:
- Creation Phase
- Sets up the Variable Environment
- Hoists declarations (see below)
- Determines
this
binding
- Execution Phase
- Executes code line by line
- Assigns values to variables and runs functions
function greet(name) { console.log(`Hello, ${name}!`) } console.log(typeof favoriteFood) // "undefined" β hoisted but uninitialized var favoriteFood = 'pizza' greet('Alice') // "Hello, Alice!"
2. Scope Chain
JavaScript uses lexical scoping, meaning the scope of a variable is determined by its position in the source code. When resolving an identifier:
- Look in the current lexical environment.
- If not found, move up to the outer environment, and so on, until the global.
- If still not found, throw a
ReferenceError
.
const globalVar = 'π' function outer() { const outerVar = 'π' function inner() { const innerVar = 'β¨' console.log(globalVar, outerVar, innerVar) } inner() // logs: "π π β¨" } outer()
This chain of environments lets inner functions close over outer variables, forming closures.
3. Hoisting
During the creation phase, JavaScript βhoistsβ declarations:
var
declarations are hoisted and initialized toundefined
.- Function declarations are hoisted with their full definition.
let
andconst
are hoisted but uninitialized, creating a Temporal Dead Zone (TDZ) until execution reaches their declaration.
Variable Hoisting
console.log(a) // undefined var a = 5 console.log(b) // ReferenceError: Cannot access 'b' before initialization let b = 10
Function Hoisting
sayHi() // "Hi!" function sayHi() { console.log('Hi!') } sayBye() // TypeError: sayBye is not a function var sayBye = function () { console.log('Bye!') }
- Function declarations (
function foo() {}
) are fully hoisted. - Function expressions assigned to
var
are hoisted as variables (undefined
), not as functions.
4. Pitfalls & Anti-Patterns
Concept | β Good Use | β Anti-Pattern |
---|---|---|
var vs let /const | Use let /const for block scope | Overusing var , causing unexpected globals |
TDZ awareness | Declare at top of block | Relying on hoisted let /const before init |
Nested scopes | Encapsulate logic in functions/blocks | Deeply nested closures without clear purpose |
Function declarations | Define functions at top or inside modules | Mixing declarations and expressions unpredictably |
Global pollution | Wrap code in IIFEs or modules | Defining many globals on window or global |
Final Thoughts
- Prefer
let
/const
overvar
to avoid hoisting surprises. - Keep scopes shallowβuse modules and blocks to encapsulate.
- Declare before use to sidestep the TDZ and reference errors.
- Leverage closures consciously, understanding the scope chain.
With a clear grasp of execution contexts, scope chain resolution, and hoisting, youβll write cleaner, more reliable JavaScript that behaves exactly as you intend.