DEV Community

Tihomir Ivanov
Tihomir Ivanov

Posted on

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
Enter fullscreen mode Exit fullscreen mode

Or wondered why this works:

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

But this doesn't:

console.log(y); // ReferenceError!
let y = 5;
Enter fullscreen mode Exit fullscreen mode

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!');
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Function Expressions Are NOT Hoisted

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

var greet = function() {
  console.log('Hello!');
};
Enter fullscreen mode Exit fullscreen mode

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!');
};
Enter fullscreen mode Exit fullscreen mode

Part 2: var Hoisting

Variables Declared with var

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

What JavaScript "sees":

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

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();
Enter fullscreen mode Exit fullscreen mode

What JavaScript "sees":

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

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')
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

const Has the Same Behavior

console.log(y); // ReferenceError
const y = 10;
Enter fullscreen mode Exit fullscreen mode

Additional const rule: Must be initialized at declaration:

const z; // SyntaxError: Missing initializer in const declaration
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

let doesn't:

let y = 1;
let y = 2; // SyntaxError: Identifier 'y' has already been declared
Enter fullscreen mode Exit fullscreen mode

const doesn't:

const z = 1;
const z = 2; // SyntaxError: Identifier 'z' has already been declared
Enter fullscreen mode Exit fullscreen mode

Reassignment Examples

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

const y = 1;
y = 2; // TypeError: Assignment to constant variable
Enter fullscreen mode Exit fullscreen mode

Important: const prevents reassignment, not mutation:

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

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')
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

let in if Blocks

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

console.log(blockScoped); // ReferenceError: blockScoped is not defined
Enter fullscreen mode Exit fullscreen mode

With var:

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

console.log(functionScoped); // "Inside" (leaked out of block!)
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

Function expressions are not:

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

const Header = () => {
  return <h1>Hello</h1>;
};
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

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>;
  }
}
Enter fullscreen mode Exit fullscreen mode

Fix with let:

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

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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!');
};
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

Fix: Define classes before using them:

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

const instance = new MyClass(); // Works
Enter fullscreen mode Exit fullscreen mode

Gotcha 3: TDZ in Parameter Defaults

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

example(); // ReferenceError: Cannot access 'b' before initialization
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Gotcha 4: typeof and TDZ

With var:

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

With let:

console.log(typeof y); // ReferenceError (TDZ!)
let y = 5;
Enter fullscreen mode Exit fullscreen mode

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';
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (0)