Michael Ouroumis logoichael Ouroumis

Demystifying JavaScript Execution Context, Scope Chain & Hoisting

Diagram illustrating JavaScript execution contexts as stacked layers with arrows showing scope chain and hoisted declarations

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, and const

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:

  1. Global Execution Context
    • Created when your script first runs
    • Creates the global object (window in browsers, global in Node) and this binding
  2. Function Execution Context
    • Created on each function invocation
    • Contains its own Variable Environment, Lexical Environment, and this
  3. Eval Execution Context
    • Created by calls to eval() (avoid using it!)

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:

  1. Look in the current lexical environment.
  2. If not found, move up to the outer environment, and so on, until the global.
  3. 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 to undefined.
  • Function declarations are hoisted with their full definition.
  • let and const 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/constUse let/const for block scopeOverusing var, causing unexpected globals
TDZ awarenessDeclare at top of blockRelying on hoisted let/const before init
Nested scopesEncapsulate logic in functions/blocksDeeply nested closures without clear purpose
Function declarationsDefine functions at top or inside modulesMixing declarations and expressions unpredictably
Global pollutionWrap code in IIFEs or modulesDefining many globals on window or global

Final Thoughts

  • Prefer let/const over var 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.

Enjoyed this post? Share it: