Learning Objectives

  • Understand what hoisting is and how it works
  • Learn how var, let, and const are hoisted differently
  • Master function hoisting vs function expressions
  • Understand the Temporal Dead Zone (TDZ)
  • Apply best practices to avoid hoisting bugs

What is Hoisting?

Hoisting is JavaScript's default behavior of moving declarations to the top of their scope before code execution. This means you can use variables and functions before they're declared in your code.

// This works!
console.log(greeting); // undefined
var greeting = "Hello";

// Behind the scenes, JavaScript does this:
var greeting;           // Declaration hoisted
console.log(greeting);  // undefined
greeting = "Hello";     // Assignment stays in place

Variable Hoisting with var

Variables declared with var are hoisted to the top of their function or global scope and initialized with undefined.

function example() {
    console.log(x); // undefined (not ReferenceError!)
    var x = 5;
    console.log(x); // 5
}

// What JavaScript actually does:
function example() {
    var x;              // Hoisted and initialized to undefined
    console.log(x);     // undefined
    x = 5;              // Assignment
    console.log(x);     // 5
}

Common var Hoisting Pitfall

var name = "Global";

function showName() {
    console.log(name); // undefined (not "Global"!)
    var name = "Local";
    console.log(name); // "Local"
}

// Why? Because JavaScript does this:
function showName() {
    var name;           // Local var hoisted, shadows global
    console.log(name);  // undefined
    name = "Local";
    console.log(name);  // "Local"
}

let and const: The Temporal Dead Zone

Variables declared with let and const are also hoisted, but they're not initialized. Accessing them before declaration causes a ReferenceError.

console.log(x); // ReferenceError: Cannot access 'x' before initialization
let x = 5;

console.log(y); // ReferenceError: Cannot access 'y' before initialization
const y = 10;

The Temporal Dead Zone (TDZ)

The TDZ is the period between entering scope and the variable declaration being executed. During this time, the variable exists but cannot be accessed.

// TDZ starts here for 'name'
console.log(name); // ReferenceError

let name = "Alice"; // TDZ ends here
console.log(name);  // "Alice"
function example() {
    // TDZ for 'temp' starts
    console.log(temp); // ReferenceError
    
    let temp = 5;      // TDZ ends
    console.log(temp); // 5
}

Function Hoisting

Function declarations are fully hoisted - both the declaration and the definition. You can call them before they appear in your code.

// This works!
greet(); // "Hello!"

function greet() {
    console.log("Hello!");
}

// JavaScript hoists the entire function
function greet() {
    console.log("Hello!");
}
greet(); // "Hello!"

Function Expressions Are NOT Hoisted

Function expressions (functions assigned to variables) follow variable hoisting rules.

// Using var
sayHi(); // TypeError: sayHi is not a function

var sayHi = function() {
    console.log("Hi!");
};

// What JavaScript does:
var sayHi;              // Hoisted, initialized to undefined
sayHi();                // TypeError: undefined is not a function
sayHi = function() {
    console.log("Hi!");
};
// Using let/const
sayHello(); // ReferenceError: Cannot access 'sayHello' before initialization

const sayHello = function() {
    console.log("Hello!");
};

Arrow Functions and Hoisting

Arrow functions follow the same hoisting rules as function expressions.

// ReferenceError
multiply(2, 3);

const multiply = (a, b) => a * b;

Class Hoisting

Classes are hoisted but not initialized, similar to let and const.

const dog = new Dog(); // ReferenceError

class Dog {
    constructor(name) {
        this.name = name;
    }
}

Practical Examples

Example 1: Loop Variable Hoisting

// With var - common bug
for (var i = 0; i < 3; i++) {
    setTimeout(() => console.log(i), 100);
}
// Prints: 3, 3, 3 (var is hoisted to function scope)

// With let - works as expected
for (let i = 0; i < 3; i++) {
    setTimeout(() => console.log(i), 100);
}
// Prints: 0, 1, 2 (let is block-scoped)

Example 2: Conditional Function Declaration

// Avoid this - behavior varies across browsers
if (true) {
    function foo() {
        return "A";
    }
} else {
    function foo() {
        return "B";
    }
}

// Use function expressions instead
let foo;
if (true) {
    foo = function() {
        return "A";
    };
} else {
    foo = function() {
        return "B";
    };
}

Example 3: Variable Shadowing

let x = 10;

function test() {
    console.log(x); // ReferenceError (not 10!)
    let x = 20;     // This shadows the outer x
}

// The let x inside test() is hoisted but in TDZ

Best Practices

1. Always Declare Variables at the Top

// Good
function calculate() {
    let result = 0;
    let temp = 5;
    
    // Use variables
    result = temp * 2;
    return result;
}

2. Use const by Default, let When Needed

// Good
const PI = 3.14159;
let counter = 0;

// Avoid var
// var x = 5; // Don't use var in modern JavaScript

3. Declare Functions Before Use

// Good - clear and predictable
const greet = function(name) {
    return `Hello, ${name}!`;
};

console.log(greet("Alice"));

4. Use Function Declarations for Top-Level Functions

// Function declarations are fine at the top level
function initialize() {
    // Setup code
}

function processData(data) {
    // Processing logic
}

// Call them
initialize();
processData(myData);

Common Hoisting Mistakes

Mistake 1: Assuming let/const Don't Hoist

// Wrong assumption: "let doesn't hoist"
let x = 10;

function test() {
    console.log(x); // ReferenceError (not 10!)
    let x = 20;     // This IS hoisted, but in TDZ
}

Mistake 2: Using Variables Before Declaration

// Bad
function process() {
    result = calculate(); // What's result?
    var result;           // Declared too late
    return result;
}

// Good
function process() {
    let result = calculate();
    return result;
}

Mistake 3: Relying on Function Hoisting

// Works but confusing
doSomething();

function doSomething() {
    // Code
}

// Better - clear order
function doSomething() {
    // Code
}

doSomething();

Interview Questions

Question 1: What will this print?

var a = 1;

function test() {
    console.log(a);
    var a = 2;
}

test();

Answer: undefined - The local var a is hoisted and shadows the global a.

Question 2: What will this print?

console.log(foo());

function foo() {
    return "A";
}

var foo = function() {
    return "B";
};

Answer: "A" - Function declaration is hoisted above the variable assignment.

Question 3: What happens here?

let x = 1;

{
    console.log(x);
    let x = 2;
}

Answer: ReferenceError - The inner let x is hoisted to the block scope but in TDZ.

Key Takeaways

  • var is hoisted and initialized to undefined
  • let/const are hoisted but not initialized (TDZ)
  • Function declarations are fully hoisted
  • Function expressions follow variable hoisting rules
  • ✅ Always declare variables at the top of their scope
  • ✅ Prefer const and let over var
  • ✅ Don't rely on hoisting - write clear, sequential code

Conclusion

Understanding hoisting helps you avoid subtle bugs and write more predictable JavaScript code. While hoisting is a core JavaScript behavior, modern best practices encourage writing code that doesn't rely on it.

Practice tip: Review your existing code for hoisting-related issues. Replace var with const or let, and declare all variables at the top of their scope.