What is Hoisting?
During the compilation phase, before any code runs, the JavaScript engine scans for all declarations and registers them in memory. This behavior is called hoisting. It does not physically move code - it is about when declarations become available.
// This works because function declarations are fully hoisted
console.log(add(2, 3)); // 5 - called BEFORE the declaration!
function add(a, b) {
return a + b;
}
// This does NOT work - function expression is not hoisted
// console.log(multiply(2, 3)); // TypeError: multiply is not a function
const multiply = (a, b) => a * b;
// Conceptually, the engine processes the code as if it were:
// function add(a, b) { return a + b; } <- hoisted to top
// const multiply = ... <- stays in place
// console.log(add(2, 3));
// console.log(multiply(2, 3));
var Hoisting
var declarations are hoisted to the top of their function scope
and initialized to undefined. This is the source of many subtle bugs.
// var is hoisted with value 'undefined'
console.log(name); // undefined (not ReferenceError!)
var name = "Alice";
console.log(name); // "Alice"
// The engine processes it as:
// var name; // hoisted - initialized to undefined
// console.log(name); // undefined
// name = "Alice"; // assignment stays here
// console.log(name); // "Alice"
// Bug: checking var before assignment
function checkUser() {
if (false) {
var user = "Alice"; // declaration is hoisted to function top!
}
console.log(user); // undefined (not ReferenceError)
// 'user' exists (hoisted) but was never assigned (if block never ran)
}
checkUser();
// var is hoisted to FUNCTION scope, not block scope
function example() {
console.log(x); // undefined (hoisted within function)
for (var x = 0; x < 3; x++) {}
console.log(x); // 3 (x leaks out of for block)
}
Function Declaration Hoisting
Function declarations are fully hoisted - both the name and the body. This is why you can call a function before its declaration in the code.
// Function declarations are fully hoisted
greet("Alice"); // "Hello, Alice!" - works before the function definition
calculate(); // works too
function greet(name) {
console.log(`Hello, ${name}!`);
}
function calculate() {
const result = add(2, 3); // can call another hoisted function
console.log(result);
}
function add(a, b) { return a + b; }
// Function EXPRESSIONS are NOT hoisted (only the var binding)
// sayHi(); // TypeError: sayHi is not a function
const sayHi = function() { console.log("Hi!"); };
// Arrow functions: same as expressions - not fully hoisted
// greetUser(); // TypeError
const greetUser = () => console.log("Hello!");
// If the same name has both var and function, function wins
var value = "original";
console.log(value); // should be "original" after assignment
// But before assignment - var hoisted as undefined vs function hoisted fully:
console.log(typeof double); // "function" (function wins over var)
var double = 42; // overwrites with 42 on this line
function double(n) { return n * 2; } // already hoisted above var
let and const Hoisting
let and const ARE hoisted (the engine knows about them), but
they are NOT initialized. The period between hoist and initialization is the
Temporal Dead Zone.
// let/const: hoisted but in TDZ until declaration line
// console.log(x); // ReferenceError: Cannot access 'x' before initialization
let x = 10;
console.log(x); // 10 (after declaration)
// This proves let IS hoisted (engine knows about it):
let scope = "outer";
{
// console.log(scope); // ReferenceError (not "outer"!)
// If let were NOT hoisted, it would see the outer 'scope'
// But it IS hoisted (to this block's scope) so it throws TDZ error
let scope = "inner";
console.log(scope); // "inner"
}
// const: same behavior as let - hoisted but in TDZ
// console.log(PI); // ReferenceError
const PI = 3.14159;
console.log(PI); // 3.14159
Temporal Dead Zone (TDZ)
The TDZ is the gap between entering a scope and reaching the let/const
declaration. Any access in this zone throws a ReferenceError.
{
// == TDZ for 'name' starts here ==
// All of these throw ReferenceError:
// console.log(name);
// typeof name; // even typeof throws in TDZ for let/const
// name = "something";
// == TDZ for 'name' ends here ==
let name = "Alice";
console.log(name); // "Alice" - safe now
}
// TDZ with function default parameters
function greet(name, greeting = name.toUpperCase()) {
// If name is in TDZ, accessing it here throws
// This is rare but possible with complex default expressions
return `${greeting}, ${name}`;
}
// Practical: TDZ catches use-before-assign bugs immediately
// With var: silent undefined bug
// With let/const: immediate ReferenceError (much easier to diagnose)
Class Hoisting
Class declarations are hoisted like let/const - they exist in the
TDZ until the declaration is reached. Unlike function declarations, you cannot use a class
before its declaration.
// Function declaration - fully hoisted, can use before definition
new Animal("Lion"); // works (if Animal were a function declaration)
// Class declaration - in TDZ, CANNOT use before definition
// const dog = new Dog(); // ReferenceError: Cannot access 'Dog' before init
class Dog {
constructor(name) {
this.name = name;
}
}
const dog = new Dog("Rex"); // OK - after the class declaration
console.log(dog.name); // "Rex"
// Class expression - same as const, not hoisted at all
// const cat = new Cat(); // ReferenceError or TypeError
const Cat = class {
constructor(name) { this.name = name; }
};
Best Practices
// 1. Always use const/let - never var
// TDZ errors are explicit and easy to find
// var's undefined behavior is a silent bug
// 2. Declare variables at the TOP of their scope
function processData(data) {
// Declare all vars here, even if used later
const result = [];
let index = 0;
// ... rest of function
}
// 3. Declare functions before calling them
// Even though declarations are hoisted, top-down readable code
// is easier to maintain
function main() {
const data = loadData(); // loadData declared below
render(process(data)); // both declared below
}
function loadData() { /* ... */ }
function process(d) { /* ... */ }
function render(d) { /* ... */ }
// 4. Use ESLint with no-use-before-define rule
// to catch hoisting issues automatically
// Hoisting cheat sheet:
// var declaration -> hoisted, initialized to undefined
// let/const declaration-> hoisted, in TDZ (ReferenceError if accessed)
// function declaration -> fully hoisted (name + body)
// function expression -> var part hoisted, assignment stays
// class declaration -> hoisted, in TDZ (like let)