Introduction
For the first 20 years of its existence, JavaScript only had a single keyword for declaring variables: var. This solitary keyword was plagued with counter-intuitive scope rules, bizarre hoisting mechanics, and silent failures that caused millions of hours of collective debugging misery.
In 2015, ES6 fundamentally repaired the language by introducing let and const. However, because JavaScript carries a strict mandate to “never break the web,” var remains in the engine to this day.
If you want to be a senior JavaScript or TypeScript developer in 2026, you cannot simply memorize “just use const.” You must deeply understand why the older mechanisms failed, how the JavaScript engine parses scope, what Hoisting actually does under the hood, and how the Temporal Dead Zone (TDZ) protects your modern memory heap.
1. The Legacy of var and Function Scope
The fundamental structural flaw of the var keyword is that it operates on Function Scope, not Block Scope.
In almost every other C-like language (Java, C++, Rust, Go), if you declare a variable inside a { } block—like an if statement or a for loop—that variable is immediately destroyed when the block ends. var completely ignores normal blocks.
// BAD: The 'var' keyword ignores the 'if' block!
function processOrder(isVIP) {
var discount = 0;
if (isVIP) {
var discount = 50; // This does NOT create a new variable!
// It overwrites the outer 'discount'
var vipCode = "XYZ";
}
// This prints 50! (Expected 0 in other languages)
console.log(discount);
// This prints 'XYZ'! vipCode leaked out of the if-block!
console.log(vipCode);
}
The only boundary that stops a var from leaking is a function() { ... }. This single architectural quirk drove developers to invent messy workarounds like IIFEs (Immediately Invoked Function Expressions) just to emulate basic block privacy.
The Global Object Binding Flaw
When you declare a var at the absolute top level of a script (not inside any function), JavaScript silently attaches that variable directly to the global window object (in browsers) or the global object (in Node.js).
var apiKey = "12345";
console.log(window.apiKey); // Outputs: "12345"
This is extremely dangerous in large single-page applications because third-party scripts loaded via CDN could easily accidentally overwrite your var global configurations.
2. Hoisting: JavaScript’s Read-Ahead Mechanism
To understand the next problem with var, we must understand Hoisting.
When the JavaScript engine (like Chrome’s V8) executes a script, it actually makes two passes over the code:
- The Creation Phase (Parsing): The engine scans the file to find all variable and function declarations, storing them in memory (the Lexical Environment).
- The Execution Phase: The engine runs the code line by line, assigning actual values.
Because the engine “finds” declarations before executing, it appears as though variable declarations are “hoisted” (pulled up) to the top of their scope.
// Given this code:
console.log(user); // Outputs: undefined (Doesn't throw an error!)
var user = "Alice";
// The V8 Engine actually interprets it like this:
var user; // Hoisted to top, implicitly initialized with 'undefined'
console.log(user); // Variable exists, but has no value yet
user = "Alice"; // Assignment remains in place
This behavior is confusing. Accessing a variable before declaring it should logically crash the program. Instead, var returns undefined, leading to silent logic bugs that are nearly impossible to trace.
3. The Modern Era: let and const
ES6 introduced let and const specifically to fix the fundamental brokenness of var. They introduce two major fixes: Block Scoping and the Temporal Dead Zone.
True Block Scope
Both let and const respect { } blocks. They cannot leak out of if statements, for loops, or standard brackets.
// GOOD: 'let' respects the block
function processOrder(isVIP) {
let discount = 0;
if (isVIP) {
let discount = 50; // Perfectly safe. Shadows the outer variable.
let vipCode = "XYZ";
}
console.log(discount); // Outputs 0. The outer variable is safe.
// console.log(vipCode); // ReferenceError: vipCode is not defined!
}
The Temporal Dead Zone (TDZ)
Do let and const get hoisted? Yes! The engine still reads them in the Creation Phase.
However, unlike var, the engine refuses to initialize them with undefined. Instead, from the start of the block until the exact line where the let/const is declared, the variable is trapped in the Temporal Dead Zone. Any attempt to read the variable inside the TDZ throws a strict, immediate ReferenceError.
console.log(username); // ReferenceError: Cannot access 'username' before initialization
let username = "Bob"; // Here the TDZ ends
Crashing the application loudly and early is infinitely better than silently returning undefined and corrupting the database later.
4. const: Mutability vs. Reassignment
By far the most misunderstood keyword in JavaScript is const.
In C++ or Rust, a constant is truly immutable; the bits in ram cannot be altered. In JavaScript, const only prevents Reassignment, it does NOT prevent Mutation.
If a const holds a primitive value (String, Number, Boolean), it acts like a true constant:
const maxLimit = 100;
maxLimit = 200; // TypeError: Assignment to constant variable.
However, if a const holds a reference type (an Object or an Array), the variable merely holds a locked pointer to a location in heap memory. You cannot change the pointer, but you can mutate the data residing at that memory address!
const user = { name: "Alice", role: "Admin" };
// This is perfectly legal and will mutate the object!
user.role = "Guest";
// This throws a TypeError, because we are trying to change the reference pointer
user = { name: "Bob", role: "User" };
const configArray = ["A", "B"];
configArray.push("C"); // Perfectly legal! Array is now ["A", "B", "C"]
How do we make true constants?
To freeze an object preventing its properties from being altered in modern JS, you must use Object.freeze():
const config = Object.freeze({ db: "Postgres", port: 5432 });
config.port = 8080; // Silent failure (or TypeError in Strict Mode)
5. Undeclared Variables (The Ultimate Anti-Pattern)
If you assign a value to a variable without using any keyword (var, let, or const), JavaScript assumes you are trying to attach a property directly to the global object, regardless of how deep within a function you are.
function calculateTaxes() {
taxRate = 0.2; // MISSING KEYWORD!
}
calculateTaxes();
// 'taxRate' is now permanently polluting the global Window object!
console.log(window.taxRate); // 0.2
To prevent this, you must always run JavaScript modules using "use strict";, or rely on ES6 Modules (which enable strict mode by default in 2026).
Best Practices Decision Guide (2026 Standard)
The hierarchical ruleset for declaring variables in any modern web application or Node.js backend is absolute:
- Default to
const: Always useconstfor everything. If an identifier is never reassigned, the engine can optimize it aggressively. It also signals to other developers that the reference binding is stable. - Fall back to
let: Useletonly for counters inforloops or variables that fundamentally mandate reassignment (e.g., flipping a boolean flag, accumulating a math sum). - Ban
varentirely: There is zero legitimate architectural reason to write the wordvarin newly authored JavaScript or TypeScript codebase today. Configure your ESLint to forcefully reject it ("no-var": "error"). Consider this seemingly innocent loop usingvar:
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log("Timer A:", i);
}, 100);
}
// Output: Timer A: 3, Timer A: 3, Timer A: 3
Why does this happen?
Because var i = 0 ignores the { } block scope of the for loop, there is actually only one single variable named i bound to the nearest function (or global) scope.
The three setTimeout callbacks are queued to run 100 milliseconds into the future. But by the time 100ms has passed, the synchronous for loop has already finished executing completely. The single value of i has been incremented to 3. When the three closures finally fire, they all point to that exact same referenced memory location of i, printing 3.
To fix this in the ES5 era, developers had to create IIFEs (Immediately Invoked Function Expressions) to trap the value manually.
The ES6 Solution with let
The let keyword famously solves this beautifully because it obeys Block Scope.
When you use let in a for loop initialization, JavaScript does something incredibly smart and unique behind the scenes: It creates a brand new, completely distinct variable binding for every single iteration of the loop.
for (let j = 0; j < 3; j++) {
setTimeout(function() {
console.log("Timer B:", j);
}, 100);
}
// Output: Timer B: 0, Timer B: 1, Timer B: 2
Each iteration gets its own lexical scope closure. The timeout callback captures its own specific “snapshot” of j for that specific iteration.
7. Global Execution Contexts and Redefinition
Another critical difference between var and let is how they react to re-declaration within the identical scope.
If you are working in a complex monolithic file built by multiple developers without strict linting, someone might accidentally declare a variable name twice.
// BAD: 'var' silently ignores redeclaration
var adminId = "X99";
var adminId = "Z44";
console.log(adminId); // Outputs: "Z44"
Because var silently overwrites itself, a junior developer might override a critically important configuration flag without knowing it.
If you attempt this with let or const, the Lexical Environment immediately halts parsing with an unrecoverable SyntaxError before any code executes.
let guestId = "U77";
let guestId = "T22"; // SyntaxError: Identifier 'guestId' has already been declared
Global Redefinition Risk:
Moreover, defining a global var shadows built-in browser APIs natively attached to the window object. If you accidentally write var name = "Alice"; in the global scope of a script tag, you have just forcefully overwritten window.name, which controls the target browsing context! let does not attach itself to the window object, keeping your built-in APIs safe from catastrophic global namespace pollution.
Best Practices Decision Guide (2026 Standard)
The hierarchical ruleset for declaring variables in any modern web application or Node.js backend is absolute:
- Default to
const: Always useconstfor everything. If an identifier is never reassigned, the engine can optimize it aggressively. It also signals to other developers that the reference binding is stable. - Fall back to
let: Useletonly for counters inforloops or variables that fundamentally mandate reassignment (e.g., flipping a boolean flag, accumulating a math sum). - Ban
varentirely: There is zero legitimate architectural reason to write the wordvarin newly authored JavaScript or TypeScript codebase today. Configure your ESLint to forcefully reject it ("no-var": "error").
Comments