JavaScript Hoisting Explained: var, let, const & Temporal Dead Zone
JavaScript Hoisting Explained: var, let, const & Temporal Dead Zone
Consider this JavaScript:
console.log(a);
var a = 10;Output:
undefinedBut if we replace var with let:
console.log(a);
let a = 10;we get:
ReferenceError: Cannot access 'a' before initializationAnd const behaves similarly:
console.log(a);
const a = 10;ReferenceError: Cannot access 'a' before initializationWhy? You may have heard:
“varis hoisted, butletandconstare not."
That’s a common shortcut, but it’s not technically accurate.
var, let, and const declarations are all processed before normal statement execution.
The important difference is how their bindings are initialized.
To understand this properly, we need to connect:
Execution Context
↓
Setup / Creation
↓
Variable Bindings
↓
var / let / const
↓
Hoisting
↓
Temporal Dead Zone1. First: var, let, and const
JavaScript provides three common ways to declare variables:
var name = "Jashan";
let age = 26;
const role = "Frontend Engineer";At a high level:
var
├── Function-scoped
├── Reassignment ✅
├── Redeclaration ✅
├── Starts as undefined
└── TDZ ❌
let
├── Block-scoped
├── Reassignment ✅
├── Redeclaration ❌
├── Starts uninitialized
└── TDZ ✅
const
├── Block-scoped
├── Reassignment ❌
├── Redeclaration ❌
├── Starts uninitialized
└── TDZ ✅We’ll understand why these differences exist rather than simply memorizing the table.
2. What Is Hoisting?
A common explanation of hoisting is:
“JavaScript moves declarations to the top.”
For example, developers sometimes imagine:
console.log(a);
var a = 10;becoming:
var a;
console.log(a);
a = 10;This produces the correct result for this simple example, but JavaScript does not literally rearrange your source code.
A better definition is:
Hoisting describes the behavior where declarations are processed and their bindings are established before normal execution reaches their declaration line.
Remember from the blog that we used this simplified model:
Execution Context
↓
Setup / Creation
↓
Code ExecutionHoisting becomes much easier to understand when connected to that setup.
3. What Happens with var?
Consider:
console.log(a);
var a = 10;
console.log(a);During setup, JavaScript establishes the var binding and initializes it with:
undefinedSimplified:
SETUP
a → undefinedThen normal execution begins.
First line
console.log(a);At this point:
a → undefinedSo:
undefinedis printed.
Next
var a = 10;During execution, the assignment happens:
a → 10Then:
console.log(a);prints:
10Complete flow:
SETUP
a → undefined
↓
EXECUTION
console.log(a) → undefined
a = 10
console.log(a) → 10This is the behavior developers commonly describe as:
“varis hoisted."
4. Declaration vs Assignment
This distinction is very important.
When we write:
var a = 10;there are conceptually two things involved:
Declaration
var a
Assignment
a = 10The declaration is processed as part of environment setup.
The assignment occurs when execution reaches that statement.
So:
console.log(a);
var a = 10;does not mean:
a → 10before the first console.log.
It means, conceptually:
Before normal execution:
a → undefined
During execution:
a → 105. What Happens with let?
Now consider:
console.log(a);
let a = 10;A common explanation says:
“letis not hoisted."
But that’s misleading.
The let binding is also established before normal execution reaches its declaration.
The difference is that it is not initialized with undefined like var.
Conceptually:
SETUP
var a → initialized with undefined
let b → uninitializedFor:
console.log(a);
let a = 10;before execution reaches:
let a = 10;the binding exists, but it is uninitialized.
Therefore, trying to access it throws:
ReferenceError:
Cannot access 'a' before initialization6. Temporal Dead Zone (TDZ)
The period between entering the relevant scope and the point where a let or const binding is initialized is called the:
Temporal Dead Zone (TDZ)
Example:
{
// TDZ for user starts
console.log(user); // ❌ ReferenceError
let user = "Jashan";
// TDZ for user has ended
console.log(user); // ✅ Jashan
}Visualize it as:
Enter Scope
│
▼
┌──────────────────────┐
│ Temporal Dead Zone │
│ │
│ user exists as a │
│ binding, but cannot │
│ be accessed yet │
└──────────────────────┘
│
▼
let user = "Jashan"
│
│ initialized
▼
user can be accessed ✅Simple definition
TDZ is the period where a
letorconstbinding exists but cannot be accessed because it has not yet been initialized.
7. Why Does TDZ Exist?
Consider:
console.log(user);
let user = "Jashan";If let behaved exactly like var, the first line might silently give:
undefinedInstead, JavaScript throws an error.
This helps catch code that accesses a variable before the point where the programmer intended it to become usable.
Compare:
console.log(total); // undefined
var total = 500;with:
console.log(total); // ReferenceError
let total = 500;The second version exposes the problem immediately.
8. What About const?
const also has a Temporal Dead Zone.
console.log(API_URL);
const API_URL = "/api/users";Result:
ReferenceError:
Cannot access 'API_URL' before initializationConceptually:
Enter Scope
↓
API_URL binding exists
but is uninitialized
↓
TDZ
↓
const API_URL = "/api/users"
↓
Initialized
↓
Accessible ✅But const has another important rule:
A
constdeclaration must have an initializer.
This works:
const role = "Admin";This doesn’t:
const role;because a const binding cannot later be reassigned to provide its initial value.
9. let vs const: Reassignment
let allows reassignment:
let status = "pending";status = "completed"; // ✅const does not:
const status = "pending";status = "completed"; // ❌ TypeErrorA useful rule for modern JavaScript is:
Use
constby default. Useletwhen the binding needs to be reassigned. Avoidvarin modern application code unless you specifically need or are explaining its behavior.
10. Important const Interview Trap
const does not make an object immutable.
Consider:
const user = {
name: "Jashan",
role: "Developer",
};
user.role = "Senior Developer"; // ✅This is allowed. Why?
Because we didn’t reassign the user binding.
The binding still refers to the same object.
user
│
▼
Object
{
name: "Jashan",
role: "Senior Developer"
}What isn’t allowed:
user = {
name: "Another User",
}; // ❌That attempts to reassign the user binding.
So remember:
constprevents reassignment of the binding. It does not automatically make the referenced value immutable.
This distinction matters constantly in React and JavaScript applications.
11. Scope + TDZ
TDZ belongs to the relevant lexical scope.
Get Jashandeep Kaur’s stories in your inbox
Join Medium for free to get updates from this writer.
Consider:
let status = "Global";
function showStatus() {
console.log(status);
let status = "Local";
}
showStatus();What happens?
A common guess is:
GlobalBut the result is:
ReferenceError:
Cannot access 'status' before initializationWhy?
Because showStatus() has its own local status binding:
let status = "Local";JavaScript’s identifier resolution finds that local binding first.
showStatus environment
│
├── status → exists but uninitialized
│
▼
FOUNDOnce JavaScript finds that local binding, it does not continue searching for the global status.
But the local binding is still in its TDZ.
Therefore:
console.log(status);throws a ReferenceError.
This beautifully connects Blog 2 with Blog 3:
Scope Chain
↓
Find nearest binding
↓
Binding found
↓
Is it initialized?
│
├── Yes → use value
│
└── No → ReferenceError12. Redeclaration
Another major difference involves declaring the same identifier again.
var
var user = "Jashan";
var user = "John";
console.log(user);Output:
Johnvar permits this kind of redeclaration in the same function/global var scope.
let
let user = "Jashan";
let user = "John";This produces:
SyntaxErrorconst
const user = "Jashan";
const user = "John";Also:
SyntaxErrorThis prevents accidental duplicate lexical declarations.
We’ll connect redeclaration, shadowing, and illegal shadowing more deeply in Blog 4.
13. undefined vs Uninitialized vs Not Defined
These terms are often confused in interviews.
undefined
console.log(a);
var a = 10;Before assignment:
a exists
a is initialized
a's value = undefinedTherefore:
undefinedUninitialized
console.log(a);
let a = 10;Before initialization:
a binding exists
but is uninitializedTrying to access it:
ReferenceErrorThis is the TDZ.
Not Defined / Unresolvable
console.log(age);when no accessible age binding exists:
Current scope ❌
↓
Outer scope ❌
↓
Global ❌
↓
EndResult:
ReferenceError: age is not definedSo:
StateMeaningundefinedBinding exists and its value is undefinedUninitializedBinding exists but cannot yet be accessedNot defined / unresolvableNo accessible binding could be found
This distinction is extremely important for interviews.
14. Function Declarations and Hoisting
Hoisting doesn’t apply only to variables.
Consider:
greet();
function greet() {
console.log("Hello");
}Output:
HelloWhy?
Function declarations are instantiated during setup so the function binding is available before execution reaches the declaration statement.
Conceptually:
SETUP
greet → function available
↓
EXECUTION
greet()
→ "Hello"This is why a function declaration can normally be called before its textual declaration.
15. Function Expression — Important Difference
Now consider:
greet();
var greet = function () {
console.log("Hello");
};What happens?
During setup:
greet → undefinedbecause greet is a var binding.
Then execution reaches:
greet();which is effectively attempting:
undefined();Result:
TypeError: greet is not a functionThis is different from:
function greet() {
console.log("Hello");
}The difference comes from how the binding is established, not because JavaScript moves one piece of source code around differently.
16. What About let / const Function Expressions?
Consider:
greet();
const greet = function () {
console.log("Hello");
};Here greet is a const binding.
Before its declaration is initialized:
greet → uninitializedSo calling it results in:
ReferenceError:
Cannot access 'greet' before initializationThe same idea applies to arrow functions:
greet();
const greet = () => {
console.log("Hello");
};Again:
ReferenceErrorbecause the important question is:
How was the variable holding the function declared?
Here it was declared with const, so TDZ rules apply.
17. Hoisting Mental Model
Don’t memorize:
JavaScript physically moves declarations
to the top of the file. ❌Instead think:
JavaScript prepares the environment
↓
Declarations are processed
↓
Bindings are established
↓
Different declarations are initialized differently
↓
Normal execution beginsSimplified:
BEFORE NORMAL EXECUTION
var → binding initialized with undefined
let → binding uninitialized
const → binding uninitialized
function declaration → function binding availableThen:
NORMAL EXECUTION
Assignments happen
Statements execute
Functions are invokedThat’s a much stronger understanding of hoisting.
18. Real-World React / TypeScript Usage
In modern React code, you’ll commonly write:
function PatientCard({ patient }: PatientCardProps) {
const fullName = `${patient.firstName} ${patient.lastName}`;
const handleOpenProfile = () => {
console.log(fullName);
};
return (
<button onClick={handleOpenProfile}>
{fullName}
</button>
);
}Notice that we use const for:
fullName
handleOpenProfilebecause those bindings are not reassigned within that render.
If something needs reassignment:
let total = 0;
for (const item of items) {
total += item.price;
}let makes sense for total.
Modern rule:
Need reassignment?
│
┌──┴──┐
│ │
No Yes
│ │
▼ ▼
const letvar is generally avoided in new application code because its function scope, redeclaration behavior, and initialization semantics can make code less predictable.
19. Common Mistakes
1. “let and const are not hoisted."
Misleading.
Their declarations are processed and bindings established before normal execution, but they remain uninitialized until their declaration is evaluated.
2. “Hoisting moves code to the top.”
Not literally.
Hoisting describes observable behavior resulting from how declarations and bindings are established before normal execution.
3. “const means immutable."
Wrong.
const user = {};
user.name = "Jashan"; // ✅const prevents binding reassignment, not object mutation.
4. “TDZ means the variable doesn’t exist.”
Wrong.
The binding exists, but it is uninitialized and inaccessible during the TDZ.
5. “undefined and TDZ are the same."
Wrong.
var → initialized with undefined
let/const in TDZ → uninitializedThese are different states.
Key Takeaways
- Hoisting does not mean JavaScript physically moves code.
- Declarations are processed before normal execution.
varbindings are initialized withundefined.letandconstbindings are initially uninitialized.letandconsttherefore have a Temporal Dead Zone.- TDZ lasts until the declaration is evaluated and the binding is initialized.
varis function-scoped.letandconstare block-scoped.letallows reassignment.constprevents reassignment of the binding.constdoes not automatically make objects immutable.letandconstcannot be redeclared in the same lexical scope in the wayvarcan.- Function declarations are available before their declaration line executes.
- Function expressions follow the hoisting/TDZ behavior of the variable declaration holding them.
undefined, uninitialized, and unresolvable identifiers are different concepts.
Quick Mental Model
SCOPE SETUP
│
┌─────────┼──────────┐
│ │ │
var let const
│ │ │
▼ ▼ ▼
initialized uninitialized uninitialized
with │ │
undefined └────┬─────┘
│
▼
TDZ
│
▼
declaration executes
│
▼
initialized








