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:
- Compilation Phase — Code is parsed, variables are registered, scope is determined
- 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 greetis hoisted (asundefined) - 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:
-
vardeclarations 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 xis 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 xdeclaration is the Temporal Dead Zone - Accessing
xin the TDZ throws aReferenceError
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:
- "Hoisting is when JavaScript moves declarations to the top of their scope during compilation"
- Contrast
varandlet/const:-
varis initialized toundefined -
letandconstremain uninitialized in the TDZ
-
- Explain TDZ: "The Temporal Dead Zone is the period between entering a scope and the variable's declaration where accessing it throws a ReferenceError"
- Best practice: "Use
constby default,letwhen reassignment is needed, and avoidvarto prevent scoping issues" - React connection: "In React, block scoping with
letandconstprevents 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)