JavaScript Hoisting

Hoisting is one of JavaScript's most misunderstood behaviors. Learn exactly what gets hoisted, how var, let, const, and function declarations differ, and how to write code that avoids hoisting surprises.

Intermediate 9 min read 9 examples

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.

JavaScript
// 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.

JavaScript
// 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.

JavaScript
// 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.

JavaScript
// 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.

JavaScript
{
    // == 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.

JavaScript
// 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

JavaScript
// 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)

Frequently Asked Questions

Hoisting is the process by which the JavaScript engine moves declarations to the top of their containing scope during the compilation phase, before code runs. It does NOT physically move code - it is about how the engine processes declarations. var declarations are hoisted and initialized to undefined. let/const are hoisted but NOT initialized (TDZ). Function declarations are fully hoisted (both declaration and definition).

Yes, let and const ARE hoisted - they exist in memory from the start of the block. But they are NOT initialized until their declaration line is reached. The period before initialization is the Temporal Dead Zone (TDZ). Accessing them in the TDZ throws a ReferenceError. This is different from var which is initialized to undefined immediately on hoist.

Function declarations (function foo() {}) are a special language feature that is completely hoisted - both the binding and the function body. Function expressions (const foo = function() {}) are just variable declarations. The variable binding is hoisted (let/const to TDZ, var to undefined), but the assignment (the function) is not hoisted - it stays on its original line.

Three rules: (1) Use const/let instead of var - TDZ errors are immediate and obvious, not silent. (2) Declare variables at the top of their scope before use. (3) Declare functions before calling them even though declarations are hoisted - it makes code readable and prevents issues when refactoring to expressions. Most linters (ESLint) catch hoisting-related issues automatically.