Understanding JavaScript Hoisting

Understanding JavaScript Hoisting

JavaScript hoisting is the process during which variable and function declarations are moved to the top of their containing scope before code executes. Even though it may appear that declarations happen later in your code, JavaScript internally handles them first during the compilation phase. This invisible reordering shapes how your code behaves at runtime.

Hoisting happens in two steps: declaration hoisting and initialization. Variables declared with var are hoisted and initialized to undefined, allowing them to be referenced before their assignment. Function declarations are fully hoisted, making them callable anywhere in their scope. Understanding hoisting helps you predict outcomes and avoid confusion—even while writing playful code.

Hoisting of var Declarations

When you declare a variable using var, the declaration is hoisted, but the assignment stays where you wrote it. That means if you access the variable before its line, you get undefined, not an error.

Here’s an example involving a magic potion:

<!DOCTYPE html>
<html>
<head>
  <title>var Hoisting</title>
</head>
<body>

<script>

  console.log('Potion:', potion); // prints "Potion: undefined"

  var potion = 'Invisibility Elixir';

  console.log('Potion after assignment:', potion);

</script>

</body>
</html>

Even though the console.log appears before the var assignment, JavaScript treats it as if var potion; was at the top—leaving it undefined until assignment. So you get undefined, not an error, showing how var behaves under hoisting.

Hoisting of Function Declarations

Function declarations are completely hoisted—including their body—so you can call the function before its declaration appears in the code.

Check out this example with a spellcaster:

<!DOCTYPE html>
<html>
<head>
  <title>Function Declaration Hoisting</title>
</head>
<body>

<script>

  castSpell();

  function castSpell() {
    console.log('✨ Spell cast successfully!');
  }

</script>

</body>
</html>

Here we call castSpell() before it’s defined in the source. Because function declarations are hoisted fully, it works perfectly. That’s one difference when hoisting functions compared to variables.

Hoisting of Function Expressions

Function expressions (especially when assigned to var) are treated like variables. Declarations are hoisted, but the function isn’t defined until that line—so calling before assignment leads to undefined or an error.

Here’s how it behaves:

<!DOCTYPE html>
<html>
<head>
  <title>Function Expression Hoisting</title>
</head>
<body>

<script>

  try {
    castSpell(); // Error: castSpell is not a function
  } catch (e) {
    console.log('Spell failed to cast:', e.message);
  }

  var castSpell = function() {
    console.log('Spell cast after creation!');
  };

  castSpell(); // Works now

</script>

</body>
</html>

Even though castSpell is declared with var, JavaScript hoists only the variable, not the function. That causes an error on the first call, until the assignment is made.

Hoisting with let and const

Unlike var, let and const declarations are hoisted but not initialized. That means they exist in the scope early, but you can’t access them before the actual declaration line. Doing so throws a ReferenceError because they’re in the Temporal Dead Zone (TDZ).

Consider this example:

<!DOCTYPE html>
<html>
<head>
  <title>let/const Hoisting</title>
</head>
<body>

<script>

  try {
    console.log('Wizard name:', wizard);
  } catch (e) {
    console.log('Error:', e.message);
  }

  let wizard = 'Merlin';
  console.log('Wizard after declaration:', wizard);

  try {
    console.log('Potion:', potion);
  } catch (e) {
    console.log('Error:', e.message);
  }

  const potion = 'Healing Brew';
  console.log('Potion after declaration:', potion);

</script>

</body>
</html>

Trying to access wizard or potion before declaration triggers a reference error—because they’re hoisted but not accessible until the code reaches their definitions.

Block Scope and Hoisting Inside Blocks

Hoisting behaves differently inside blocks like if or for. Variables declared with var are hoisted to the containing function or global scope, while let and const stay within block scope.

Imagine a guard tower where declarations are scoped inside the tower:

<!DOCTYPE html>
<html>
<head>
  <title>Block Scope Hoisting</title>
</head>
<body>

<script>

  if (true) {
    var treasure = 'Gold';
    let guard = 'Elf';
    const secret = 'Hidden Map';
  }

  console.log('Treasure:', treasure); // Gold

  try {
    console.log('Guard:', guard);
  } catch (e) {
    console.log('Error:', e.message); // guard is not defined
  }

  try {
    console.log('Secret:', secret);
  } catch (e) {
    console.log('Error:', e.message); // secret is not defined
  }

</script>

</body>
</html>

Here treasure is accessible outside the if block because var hoists to the outer function scope. But guard and secret are block-scoped via let and const, making them unavailable outside.

Hoisting of Class Declarations

Class declarations are hoisted in a manner similar to let and const: they exist in scope early but aren’t initialized until the declaration line, so accessing them too soon causes a ReferenceError.

Here’s a wizard class example:

<!DOCTYPE html>
<html>
<head>
  <title>Class Hoisting</title>
</head>
<body>

<script>

  try {
    const mage = new Wizard();
  } catch (e) {
    console.log('Error:', e.message);
  }

  class Wizard {

    constructor() {
      this.name = 'Gandalf';
      console.log('Wizard created:', this.name);
    }

  }

  const mage = new Wizard();

</script>

</body>
</html>

Attempting to instantiate the class before its declaration fails, because the class isn’t initialized until its line—and that reflects how hoisting treats classes.

Combined Hoisting Example (Playful Story)

Let’s combine everything into one fun, magical storyline:

<!DOCTYPE html>
<html>
<head>
  <title>Hoisting Story</title>
</head>
<body>

<script>
  // At this point, "potion" is hoisted (because it's declared with var),
  // so it exists but holds "undefined"
  console.log('Potion:', potion); // undefined

  // Assigning a value after the first log
  potion = 'Invisibility Potion';

  // "creature" is declared with let, so it's in the "temporal dead zone"
  // Accessing it before declaration causes a ReferenceError
  try {
    console.log('Creature:', creature); // ReferenceError
  } catch(e) {
    console.log('Error:', e.message);
  }

  // The function summon is hoisted completely (declaration and body),
  // so we can call it even before its place in the code
  // BUT it uses "creature", which isn't defined yet here, so it would throw if uncommented
  // console.log('Summon:', summon()); // Would fail because "creature" isn't defined yet

  var potion; // Hoisted to the top with "undefined"
  let creature = 'Dragon'; // Not hoisted like var; lives in temporal dead zone

  function summon() {
    return 'Summoned a ' + creature;
  }

  // Now that "creature" is defined, summon works just fine
  console.log('Summon:', summon()); // "Summoned a Dragon"

  // Class expressions are not hoisted; they exist only after this line
  const wizard = class Wizard {};

  console.log('Wizard class exists now:', typeof wizard); // "function"
</script>

</body>
</html>

This code shows how hoisting works in JavaScript. Variables declared with var (like potion) are lifted to the top and get the value undefined until you assign them. But variables declared with let (like creature) are also lifted but stay in a special “dead zone” and cause an error if used too early. Functions (like summon) are fully hoisted, so you can use them before they’re written in the code — as long as the things they use are ready. Classes (like wizard) are not hoisted at all. They only exist after you create them. This shows how different types of declarations behave before and after they are defined.

Conclusion

Understanding hoisting in JavaScript means knowing which declarations get moved above and how they behave at runtime. var variables are hoisted and initialized to undefined, functions are fully hoisted, and function expressions and var‑assigned functions behave differently. let, const, and classes are hoisted but enter the TDZ until initialization. Seeing hoisting in action helps clarify why code runs the way it does—even when declaration order surprises you.

References

If you’re curious to explore further or want to double-check what you’ve learned, these trusted documentation pages offer more detailed explanations and examples:

Scroll to Top