Getting started General
Components
Forms
Trends
Utilities
Plugins Sass Migrate from v1
  Join us
  JavaScript Articles

JavaScript Hoisting: Complete Guide to Variable and Function Hoisting

JavaScript

Learn all you need to know about JavaScript hoisting with this guide. You'll know how var, let, const, and function declarations are hoisted. You'll also understand the Temporal Dead Zone, and avoid the most common traps with our examples.

JavaScript Hoisting: Complete Guide to Variable and Function Hoisting
JavaScript Hoisting: Complete Guide to Variable and Function Hoisting

JavaScript hoisting is one of the most important concepts to understand. In particular if you're working with this language. There is a difference with other programming languages. In JavaScript, variable and function declarations are "hoisted" to the top of their containing scope before the code is executed. This behavior can lead to strange/unexpected results if not properly understood.

In this guide, we'll cover all you need to know about JavaScript hoisting. We will start by explaining how var and function declarations are hoisted. Then, we will see how let and const behave. Finally, we will see how to write better JavaScript code and avoid common issues.

What is JavaScript Hoisting?

Hoisting is a JavaScript specificity where the variable and function declarations are moved to the top of their containing scope before the code is executed. It means you can use variables and functions before they are declared in your code.

However, it's important to understand that only the declarations are hoisted, not the initializations. For variables declared with var, they are hoisted and initialized with undefined. For functions, the entire function declaration is hoisted, making it available throughout the scope.

Basic Hoisting Example

// This code works because of hoisting
console.log(x); // undefined (not an error!)
var x = 5;

// This is how JavaScript interprets the code above:
var x;          // Declaration is hoisted
console.log(x); // undefined
x = 5;          // Initialization stays in place

How Function Hoisting Works

Function declarations are completely hoisted in JavaScript. It means both the declaration and the function body are moved to the top of the scope. It means you can call functions before they are defined in your code.

Function Declaration Hoisting

// This works because function declarations are hoisted
greet(); // "Hello, World!"

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

// This is how JavaScript interprets it:
function greet() {
  console.log("Hello, World!");
}
greet(); // "Hello, World!"

Function Expression vs Function Declaration

It's crucial to understand that function expressions are NOT hoisted. Only function declarations are hoisted. This is an important distinction:

// Function declaration - HOISTED
sayHello(); // "Hello!"

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

// Function expression - NOT HOISTED
sayGoodbye(); // TypeError: sayGoodbye is not a function

var sayGoodbye = function() {
  console.log("Goodbye!");
};

// How JavaScript interprets the function expression:
var sayGoodbye;        // Declaration hoisted, initialized with undefined
sayGoodbye();          // TypeError: sayGoodbye is undefined
sayGoodbye = function() {
  console.log("Goodbye!");
};

Why Function Hoisting is Useful

Function hoisting allows you to organize your code by placing the main algorithm at the top and helper functions below, which can improve code readability:

function processItems(items) {
  // Main algorithm at the top
  return items
    .filter(itemMatches)
    .map(transformItem);

  // Helper functions below
  function itemMatches(item) {
    return item.status === 'active';
  }

  function transformItem(item) {
    return {
      id: item.id,
      name: item.name.toUpperCase()
    };
  }
}

// This works because both functions are hoisted
const result = processItems([
  { id: 1, name: 'Alice', status: 'active' },
  { id: 2, name: 'Bob', status: 'inactive' }
]);
console.log(result); // [{ id: 1, name: 'ALICE' }]

Variable Hoisting with var

Variables declared with var are hoisted to the top of their function scope (or global scope if declared outside a function). However, only the declaration is hoisted, not the initialization. The variable is initialized with undefined until the actual assignment line is executed.

Basic var Hoisting

function example() {
  console.log(x); // undefined (not an error!)
  var x = 10;
  console.log(x); // 10
}

// How JavaScript interprets this:
function example() {
  var x;           // Declaration hoisted
  console.log(x);  // undefined
  x = 10;          // Initialization stays in place
  console.log(x);  // 10
}

The Problem with var Hoisting

The hoisting behavior of var can lead to unexpected bugs, especially in loops with asynchronous operations:

const people = ['Alice', 'Bob', 'Claire', 'David'];

for (var i = 0; i < people.length; i++) {
  setTimeout(function() {
    console.log(people[i]); // Outputs: undefined (4 times)
  }, 100);
}

// Why this happens:
// The variable 'i' is hoisted to the function scope
// By the time the setTimeout callbacks execute, the loop has finished
// and 'i' equals people.length (4), which is out of bounds

// How JavaScript interprets this:
var i;  // Hoisted to function scope
for (i = 0; i < people.length; i++) {
  setTimeout(function() {
    console.log(people[i]); // All callbacks see the same 'i' value
  }, 100);
}
// After loop completes, i = 4, so all callbacks log undefined

The Legacy Solution: IIFE

Before ES2015, developers used Immediately Invoked Function Expressions (IIFE) to create a new scope for each iteration:

const people = ['Alice', 'Bob', 'Claire', 'David'];

for (var i = 0; i < people.length; i++) {
  (function(index) {
    setTimeout(function() {
      console.log(people[index]); // Works correctly
    }, 100);
  })(i);
}

// Output: Alice, Bob, Claire, David (in order)

Block Scope and ES2015+ Variables

ES2015 introduced let and const, which have fundamentally different hoisting behavior compared to var. These new keywords provide block-level scoping and are not hoisted in the same way.

Block Scope with let and const

Variables declared with let and const are scoped to the block in which they are declared, not the entire function:

function demonstrateScope() {
  if (true) {
    var functionScoped = 'I am function scoped';
    let blockScoped = 'I am block scoped';
    const alsoBlockScoped = 'I am also block scoped';
  }

  console.log(functionScoped); // "I am function scoped" (works!)
  console.log(blockScoped);    // ReferenceError: blockScoped is not defined
  console.log(alsoBlockScoped); // ReferenceError: alsoBlockScoped is not defined
}

Solving the Loop Problem with let

The loop problem with var is easily solved by using let, which creates a new binding for each iteration:

const people = ['Alice', 'Bob', 'Claire', 'David'];

for (let i = 0; i < people.length; i++) {
  setTimeout(function() {
    console.log(people[i]); // Works correctly!
  }, 100);
}

// Output: Alice, Bob, Claire, David (in order)

// Why this works:
// Each iteration creates a new 'i' binding in its own block scope
// Each setTimeout callback captures its own 'i' value

The Temporal Dead Zone (TDZ)

The Temporal Dead Zone (TDZ) is the period between entering a scope and the actual declaration of a variable. During this time, the variable cannot be accessed. This applies to let, const, and class declarations.

Understanding the Temporal Dead Zone

// Temporal Dead Zone starts here
console.log(myVar); // ReferenceError: Cannot access 'myVar' before initialization

let myVar = 10;
// Temporal Dead Zone ends here

// This is different from var:
console.log(myVar2); // undefined (not an error)
var myVar2 = 10;

Why the Temporal Dead Zone Exists

The TDZ helps catch programming errors early by preventing access to variables before they are initialized. This makes the code more predictable and easier to debug:

function example() {
  // TDZ starts
  console.log(typeof myLet); // ReferenceError (not "undefined")
  
  let myLet = 'initialized';
  // TDZ ends
}

// Compare with var:
function example2() {
  console.log(typeof myVar); // "undefined" (no error)
  var myVar = 'initialized';
}

Hoisting Order and Priority

When multiple declarations exist in the same scope, JavaScript hoists them in a specific order:

  1. Function declarations are hoisted first
  2. Variable declarations (var) are hoisted second
  3. Function expressions are not fully hoisted (only the variable declaration)

Hoisting Order Example

// What you write:
console.log(typeof myFunc); // "function"
console.log(typeof myVar);  // "undefined"

function myFunc() {
  return 'I am a function';
}

var myVar = 'I am a variable';

// How JavaScript interprets it:
function myFunc() {  // Function hoisted first
  return 'I am a function';
}
var myVar;            // Variable declaration hoisted second

console.log(typeof myFunc); // "function"
console.log(typeof myVar);  // "undefined"
myVar = 'I am a variable';

Function vs Variable Name Conflicts

When a function and a variable share the same name, the function declaration takes priority:

console.log(typeof myName); // "function" (not "undefined")

function myName() {
  return 'I am a function';
}

var myName = 'I am a variable';

console.log(typeof myName); // "string" (after assignment)

// How JavaScript interprets this:
function myName() {  // Function hoisted first
  return 'I am a function';
}
var myName;          // Variable declaration (ignored, name already exists)

console.log(typeof myName); // "function"
myName = 'I am a variable'; // Assignment overwrites the function
console.log(typeof myName); // "string"

Best Practices and Modern JavaScript

Understanding hoisting is crucial, but modern JavaScript best practices recommend avoiding the pitfalls associated with hoisting:

1. Avoid var - Use let and const Instead

Since ES2015, var should be avoided in favor of let and const:

// ❌ Avoid var
function badExample() {
  if (true) {
    var x = 10;
  }
  console.log(x); // 10 (unexpected function scope)
}

// ✅ Use let or const
function goodExample() {
  if (true) {
    let x = 10;
  }
  console.log(x); // ReferenceError (expected block scope)
}

2. Use const by Default

The modern JavaScript approach is to use const by default and only use let when you need to reassign the variable:

// ✅ Use const by default
const userName = 'Alice';
const userAge = 25;
const userRoles = ['admin', 'user'];

// Only use let when reassignment is needed
let counter = 0;
counter = 1; // Valid reassignment

// const prevents accidental reassignment
const maxItems = 100;
maxItems = 200; // TypeError: Assignment to constant variable

3. Declare Variables at the Top of Their Scope

While hoisting makes it possible to use variables before declaration, it's better practice to declare them at the top of their scope for clarity:

// ❌ Confusing due to hoisting
function confusing() {
  console.log(value);
  var value = 10;
}

// ✅ Clear and explicit
function clear() {
  const value = 10;
  console.log(value);
}

4. Use Function Declarations for Hoisting Benefits

Function declarations can be useful when you want to organize code with the main algorithm at the top:

// ✅ Good use of function hoisting
function processData(data) {
  // Main algorithm at top
  const filtered = data.filter(isValid);
  const transformed = filtered.map(transform);
  return transformed;

  // Helper functions below
  function isValid(item) {
    return item.status === 'active';
  }

  function transform(item) {
    return { id: item.id, name: item.name.toUpperCase() };
  }
}

Common Hoisting Pitfalls and Solutions

Pitfall 1: Accessing Variables Before Declaration

// ❌ Problem with var
function problem() {
  console.log(x); // undefined (confusing!)
  var x = 5;
}

// ✅ Solution: Use let/const
function solution() {
  // console.log(x); // ReferenceError (clear error message)
  const x = 5;
  console.log(x); // 5
}

Pitfall 2: Loop Variables in Async Callbacks

// ❌ Problem with var
const items = ['a', 'b', 'c'];
for (var i = 0; i < items.length; i++) {
  setTimeout(() => console.log(items[i]), 100);
}
// Output: undefined, undefined, undefined

// ✅ Solution: Use let
const items = ['a', 'b', 'c'];
for (let i = 0; i < items.length; i++) {
  setTimeout(() => console.log(items[i]), 100);
}
// Output: a, b, c

Pitfall 3: Function Expression vs Declaration

// ❌ Function expression not hoisted
myFunction(); // TypeError: myFunction is not a function

var myFunction = function() {
  console.log('Hello');
};

// ✅ Function declaration is hoisted
myFunction(); // "Hello"

function myFunction() {
  console.log('Hello');
}

Class Hoisting

ES2015 introduced class declarations, which behave similarly to let and const:

// ❌ Class is in TDZ before declaration
const instance = new MyClass(); // ReferenceError: Cannot access 'MyClass' before initialization

class MyClass {
  constructor() {
    this.value = 10;
  }
}

// ✅ Use class after declaration
class MyClass {
  constructor() {
    this.value = 10;
  }
}

const instance = new MyClass(); // Works correctly

Real-World Examples

Example 1: Organizing Code with Function Hoisting

// Main algorithm at the top, helpers below
function validateUserData(userData) {
  // Main validation logic
  if (!isValidEmail(userData.email)) {
    return { valid: false, error: 'Invalid email' };
  }

  if (!isValidAge(userData.age)) {
    return { valid: false, error: 'Invalid age' };
  }

  if (!isValidName(userData.name)) {
    return { valid: false, error: 'Invalid name' };
  }

  return { valid: true };

  // Helper functions (hoisted, so they work above)
  function isValidEmail(email) {
    return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
  }

  function isValidAge(age) {
    return typeof age === 'number' && age >= 0 && age <= 120;
  }

  function isValidName(name) {
    return typeof name === 'string' && name.trim().length >= 2;
  }
}

// Usage
const result = validateUserData({
  email: '[email protected]',
  age: 25,
  name: 'Alice'
});
console.log(result); // { valid: true }

Example 2: Avoiding Common Loop Pitfalls

// Modern approach with let
function attachEventListeners(buttons) {
  for (let i = 0; i < buttons.length; i++) {
    buttons[i].addEventListener('click', function() {
      console.log(`Button ${i} clicked`); // Each button logs its correct index
    });
  }
}

// Even better: Use forEach
function attachEventListenersModern(buttons) {
  buttons.forEach((button, index) => {
    button.addEventListener('click', function() {
      console.log(`Button ${index} clicked`);
    });
  });
}

Summary: Key Takeaways

  • Function declarations are fully hoisted and can be called before they appear in code
  • var declarations are hoisted but initialized with undefined
  • let and const are in the Temporal Dead Zone until their declaration line
  • Function expressions are not hoisted (only the variable declaration is)
  • Use const by default, let when reassignment is needed, and avoid var
  • Function hoisting can be useful for organizing code with main logic at the top
  • Block scope with let and const prevents many common bugs

Conclusion

JavaScript hoisting is a fundamental concept that every JavaScript developer should understand. While it can lead to unexpected behavior with var, modern JavaScript best practices using let and const help avoid these issues.

Function hoisting can be a useful feature for code organization, allowing you to place the main algorithm at the top of a function with helper functions below. However, it's important to understand the differences between function declarations and function expressions.

By understanding hoisting behavior, the Temporal Dead Zone, and following modern JavaScript best practices, you can write more predictable and maintainable code. Remember to use const by default, let when needed, and avoid var in modern JavaScript code.

Start Building with Axentix

Ready to create amazing websites? Get started with Axentix framework today.

Get Started

Related Posts