Understanding Hoisting in JavaScript
Concise, original guide to JavaScript hoisting: variable, function, and class hoisting; var vs let vs const; temporal dead zone; and best practices.
Drake Nguyen
Founder · System Architect
Introduction
This article explains javascript hoisting: how declarations are handled by the engine before code runs, and why you sometimes see undefined, ReferenceError, or unexpected behavior. We cover variable hoisting, function hoisting, class hoisting, and the ES6 changes that introduce the temporal dead zone.
Hoisting is the process where declarations (but not necessarily initializations) are processed before the JavaScript code executes, effectively moving those declarations to the top of their scope.
undefined vs ReferenceError
Knowing the difference between undefined and ReferenceError helps when debugging hoisting javascript issues. typeof on an undeclared-but-hoisted variable returns "undefined", while accessing a truly undeclared variable throws a ReferenceError.
// typeof returns "undefined" for hoisted var declarations
console.log(typeof x); // "undefined"
// direct access throws if the identifier was never declared
console.log(y); // ReferenceError: y is not defined
These behaviors reflect the interpreters treatment of declaration vs initialization and are central to understanding variable lifecycle in the execution context and scope chain.
Hoisting variables
Variable hoisting varies depending on how you declare the variable. In general, declarations are processed before execution; assignments stay where they are. That distinction explains many surprises when developers try to use a variable before its assigned.
// var hoisting example
console.log(message); // undefined
var message = 'Hello';
Also beware of implicit globals: assigning to an undeclared name creates a global at assignment time (unless strict mode is enabled), which is different from a hoisted declaration.
function demo(){
implicit = 42; // creates a global variable (unless in strict mode)
var local = 10;
}
demo();
console.log(implicit); // 42
console.log(local); // ReferenceError: local is not defined
ES5
var
Variables declared with var are function-scoped (or global if declared outside a function). The declaration is hoisted and initialized with undefined during the creation phase of the execution context. This is commonly referred to as var hoisting.
// function scope and var hoisting
function f(){
console.log(a); // undefined
var a = 5;
}
f();
Strict mode
Enabling strict mode reduces silent errors. In strict mode, assigning to an undeclared identifier throws a ReferenceError instead of creating an implicit global.
'use strict';
function g(){
undeclared = 7; // ReferenceError in strict mode
}
ES6
let
let creates block-scoped bindings. Although engines register the existence of a let binding during compilation, it remains uninitialised until its declaration is evaluated. Accessing it before that moment produces a ReferenceError; this window is called the temporal dead zone (TDZ). This behavior is often called let hoisting, but it differs from var hoisting because the binding is uninitialised.
console.log(t); // ReferenceError: cannot access 't' before initialization
let t = 3;
let u;
console.log(u); // undefined (declared & initialised)
const
const behaves similarly to let with block scope and TDZ, but it also requires an initializer. Reassigning a const raises a TypeError. The rules around const hoisting are the same as let: the binding exists ahead of execution but is uninitialised until evaluated.
const PI = 3.14;
PI = 22/7; // TypeError: Assignment to constant variable.
console.log(C); // ReferenceError: cannot access 'C' before initialization
const C = 1; // Syntax/Reference rules require an initializer
Hoisting functions
Function hoisting depends on how the function is defined.
Function declarations
Function declarations are hoisted completely: the name and the implementation are available throughout the scope, so you can call them before their appearance in source order. This is classic function hoisting.
greet(); // Works
function greet(){
console.log('hi');
}
Function expressions
Function expressions assigned to variables behave like variable hoisting: the variable declaration is hoisted but the assignment is not. Trying to invoke the expression before the assignment results in a TypeError because the identifier is not a function yet.
say(); // TypeError: say is not a function
var say = function(){
console.log('hello');
};
Order of precedence
When both functions and variables share the same name, the JavaScript engine applies consistent rules:
- Variable assignments take precedence over function declarations at runtime.
- Function declarations take precedence over variable declarations (not assignments) during hoisting.
// variable assignment overrides function
var double = 22;
function double(n){ return n*2 }
console.log(typeof double); // "number"
// function declaration still wins over a bare var declaration
var triple;
function triple(n){ return n*3 }
console.log(typeof triple); // "function"
Hoisting classes
Class declarations are hoisted in the sense that the binding exists, but they remain uninitialised until evaluation. Accessing a class before its declaration results in a ReferenceError (similar to TDZ behavior). Class expressions are not hoisted.
// class declaration
new Thing(); // ReferenceError: Cannot access 'Thing' before initialization
class Thing { constructor(){} }
// class expression
var Ctr = class {};
new Ctr(); // ok after assignment
Caveat
Theres nuance in the terminology: some developers say let/const are hoisted, others prefer "registered but uninitialised". The important point is to understand initialization vs declaration and the temporal dead zone when reasoning about let hoisting and const hoisting.
Conclusion
Key takeaways for working with javascript hoisting:
- Declare variables before use to avoid surprises: prefer let and const for block scope and safety.
- Understand that var hoisting initializes to undefined, while let/const remain uninitialized until their statement runs (TDZ).
- Function declarations are fully hoisted; function expressions follow variable hoisting rules.
- Use strict mode to catch implicit globals and other silent errors.
Mastering hoisting, scope chain, and the execution context will make debugging easier and help you write clearer, more predictable JavaScript code.