Hoisting & The Temporal Dead Zone: Why `let` and `const` Behave Differently Than `var`

You’ve probably seen this error before:

ReferenceError: Cannot access 'myVariable' before initialization

Or wondered why this works:

console.log(x); // undefined (not an error!)
var x = 5;

But this doesn’t:

console.log(y); // ReferenceError!
let y = 5;

The answer lies in hoisting and the Temporal Dead Zone (TDZ) — two of JavaScript’s most misunderstood features.

The Golden Rule

All declarations (var, let, const, function, class) are hoisted to the top of their scope during the compilation phase, but only var and function declarations are initialized. let and const remain in the Temporal Dead Zone until their declaration is executed.

In simpler terms: JavaScript knows about your variables before you declare them, but let and const can’t be accessed until the exact line where they’re declared.

Let’s break this down step by step.

Part 1: What is Hoisting?

JavaScript runs in two phases:

  1. Compilation Phase — Code is parsed, variables are registered, scope is determined
  2. Execution Phase — Code runs line by line

Hoisting is what happens during the compilation phase: JavaScript moves declarations to the top of their scope (conceptually — it doesn’t literally move code).

Function Hoisting

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

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

Why this works:

  • The entire function is hoisted (both declaration and body)
  • You can call it before the line where it’s defined

What JavaScript “sees”:

function greet() { // Hoisted to top
  console.log('Hello!');
}

greet(); // Now it makes sense

Function Expressions Are NOT Hoisted

greet(); // TypeError: greet is not a function

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

Why this fails:

  • var greet is hoisted (as undefined)
  • But the function assignment happens later
  • You’re trying to call undefined(), which is an error

What JavaScript “sees”:

var greet; // Hoisted, initialized to undefined
greet();   // Calling undefined()

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

Part 2: var Hoisting

Variables Declared with var

console.log(x); // undefined (not an error!)
var x = 5;
console.log(x); // 5

What JavaScript “sees”:

var x; // Hoisted and initialized to undefined
console.log(x); // undefined
x = 5; // Assignment happens here
console.log(x); // 5

Key Points:

  • var declarations are hoisted to the top of their function scope (or global scope)
  • They’re initialized to undefined
  • The assignment stays where it is

var is Function-Scoped, Not Block-Scoped

function example() {
  if (true) {
    var x = 5;
  }
  console.log(x); // 5 (accessible outside the block!)
}

example();

What JavaScript “sees”:

function example() {
  var x; // Hoisted to function scope
  if (true) {
    x = 5;
  }
  console.log(x); // 5
}

Problem: var ignores block scope (if, for, while), which leads to unexpected behavior:

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// Logs: 3, 3, 3 (all callbacks see the same 'i')

Part 3: let and const Hoisting

Here’s the twist: let and const are hoisted, but they’re not initialized.

The Temporal Dead Zone (TDZ)

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

What’s happening:

  • let x is hoisted to the top of the block scope
  • But it’s not initialized (no default value)
  • The time between the start of the block and the let x declaration is the Temporal Dead Zone
  • Accessing x in the TDZ throws a ReferenceError

Visualizing the TDZ

{
  // TDZ starts here for 'x'
  console.log(x); // ReferenceError
  // TDZ continues...
  // TDZ ends here
  let x = 5; // 'x' is now initialized
  console.log(x); // 5
}

The TDZ is temporal (time-based), not spatial (location-based):

{
  const func = () => console.log(x); // Defined before 'x'
  let x = 5;
  func(); // Works! (called after 'x' is initialized)
}

const Has the Same Behavior

console.log(y); // ReferenceError
const y = 10;

Additional const rule: Must be initialized at declaration:

const z; // SyntaxError: Missing initializer in const declaration

Part 4: var vs let vs const

Comparison Table

Feature var let const
Scope Function-scoped Block-scoped Block-scoped
Hoisting Yes (initialized to undefined) Yes (but uninitialized) Yes (but uninitialized)
TDZ No Yes Yes
Re-declaration Allowed Not allowed Not allowed
Reassignment Allowed Allowed Not allowed
Initialization Required No No Yes

Re-declaration Examples

var allows re-declaration:

var x = 1;
var x = 2; // No error
console.log(x); // 2

let doesn’t:

let y = 1;
let y = 2; // SyntaxError: Identifier 'y' has already been declared

const doesn’t:

const z = 1;
const z = 2; // SyntaxError: Identifier 'z' has already been declared

Reassignment Examples

let x = 1;
x = 2; // Allowed

const y = 1;
y = 2; // TypeError: Assignment to constant variable

Important: const prevents reassignment, not mutation:

const obj = { name: 'Alice' };
obj.name = 'Bob'; // Mutation allowed
obj = {};         // Reassignment forbidden

Part 5: Block Scoping in Practice

let in Loops

for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// Logs: 0, 1, 2 (each callback has its own 'i')

Why this works: let creates a new binding for each iteration.

Compare to var:

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// Logs: 3, 3, 3 (all callbacks share the same 'i')

let in if Blocks

if (true) {
  let blockScoped = 'Inside';
  console.log(blockScoped); // "Inside"
}

console.log(blockScoped); // ReferenceError: blockScoped is not defined

With var:

if (true) {
  var functionScoped = 'Inside';
  console.log(functionScoped); // "Inside"
}

console.log(functionScoped); // "Inside" (leaked out of block!)

Part 6: Hoisting in React

While modern React doesn’t rely heavily on hoisting quirks, understanding it helps avoid bugs.

1. Component Definition Order

Function declarations are hoisted:

function App() {
  return <Header />; // Works
}

function Header() {
  return <h1>Hello</h1>;
}

Function expressions are not:

function App() {
  return <Header />; // ReferenceError (or undefined)
}

const Header = () => {
  return <h1>Hello</h1>;
};

Fix: Define components before using them, or use function declarations.

2. useState and Hoisting

function Counter() {
  console.log(count); // ReferenceError! (TDZ)

  const [count, setCount] = useState(0);

  return <div>{count}</div>;
}

Why this fails: const is in the TDZ before its declaration.

This is fine:

function Counter() {
  const [count, setCount] = useState(0);

  console.log(count); // 0

  return <div>{count}</div>;
}

3. Closure Issues with var in React

Old pattern (before hooks):

class Counter extends React.Component {
  state = { count: 0 };

  componentDidMount() {
    for (var i = 0; i < 3; i++) {
      setTimeout(() => {
        console.log(i); // Logs: 3, 3, 3
      }, i * 1000);
    }
  }

  render() {
    return <div>{this.state.count}</div>;
  }
}

Fix with let:

for (let i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i); // Logs: 0, 1, 2
  }, i * 1000);
}

4. Event Handlers and Block Scope

function Form() {
  const [value, setValue] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();

    if (value) {
      const message = `Submitted: ${value}`; // Block-scoped
      alert(message);
    }

    // console.log(message); // ReferenceError (outside block)
  };

  return (
    <form onSubmit={handleSubmit}>
      <input value={value} onChange={(e) => setValue(e.target.value)} />
      <button>Submit</button>
    </form>
  );
}

Key Point: Block-scoped variables (let, const) are contained to their blocks, preventing accidental usage outside.

Part 7: Common Hoisting Gotchas

Gotcha 1: Function Declarations vs Expressions

// Function declaration (hoisted)
function sayHi() {
  console.log('Hi!');
}

// Function expression (not hoisted)
const sayBye = function() {
  console.log('Bye!');
};

// Arrow function (not hoisted)
const sayHello = () => {
  console.log('Hello!');
};

Only function declarations are fully hoisted.

Gotcha 2: Class Hoisting

Classes are hoisted but remain in the TDZ like let and const:

const instance = new MyClass(); // ReferenceError

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

Fix: Define classes before using them:

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

const instance = new MyClass(); // Works

Gotcha 3: TDZ in Parameter Defaults

function example(a = b, b = 2) {
  console.log(a, b);
}

example(); // ReferenceError: Cannot access 'b' before initialization

Why? When evaluating a = b, b is still in the TDZ (parameters are evaluated left-to-right).

Fix:

function example(b = 2, a = b) {
  console.log(a, b);
}

example(); // 2, 2

Gotcha 4: typeof and TDZ

With var:

console.log(typeof x); // "undefined" (no error)
var x = 5;

With let:

console.log(typeof y); // ReferenceError (TDZ!)
let y = 5;

Surprising, right? Even typeof can’t save you from the TDZ.

Gotcha 5: Accessing Variables from Outer Scope

let x = 'outer';

{
  console.log(x); // ReferenceError
  let x = 'inner';
}

Why? The inner let x is hoisted to the top of the block, creating a TDZ. JavaScript doesn’t look at the outer x.

Fix: Don’t shadow variables if you need the outer value:

let x = 'outer';

{
  console.log(x); // "outer"
  let y = 'inner'; // Different name
}

Quick Reference Cheat Sheet

Scenario var let const
Accessed before declaration undefined ReferenceError (TDZ) ReferenceError (TDZ)
Scope Function Block Block
Re-declare in same scope Allowed Not allowed Not allowed
Reassign Allowed Allowed Not allowed
Mutate object properties N/A N/A Allowed

Key Takeaways

All declarations are hoisted, but only var and function declarations are initialized
let and const are in the TDZ from the start of the block until their declaration
Accessing a variable in the TDZ throws ReferenceError, not undefined
var is function-scoped; let and const are block-scoped
Use const by default, let when reassignment is needed, avoid var
const prevents reassignment, not mutation of objects/arrays
Classes are hoisted but remain in the TDZ like let and const

Interview Tip

When asked about hoisting, explain it clearly:

  1. “Hoisting is when JavaScript moves declarations to the top of their scope during compilation”
  2. Contrast var and let/const:
    • var is initialized to undefined
    • let and const remain uninitialized in the TDZ
  3. Explain TDZ: “The Temporal Dead Zone is the period between entering a scope and the variable’s declaration where accessing it throws a ReferenceError”
  4. Best practice: “Use const by default, let when reassignment is needed, and avoid var to prevent scoping issues”
  5. React connection: “In React, block scoping with let and const prevents common closure bugs, especially in loops and event handlers”

Now go forth and never be confused by ReferenceError: Cannot access before initialization again!

DevegygiebyOL
DevegygiebyOL
Articles: 1190