let vs const vs var
JavaScript gives you three keywords to declare variables. Understanding the difference between them — scope rules, hoisting behaviour, and reassignment semantics — is foundational knowledge for any developer working with modern JavaScript or TypeScript.
TL;DR — Quick Comparison
| Property | var | let | const |
|---|---|---|---|
| Scope | Function (or global) | Block | Block |
| Hoisting | Yes — initialised as undefined | Yes — but in TDZ until declaration | Yes — but in TDZ until declaration |
| Reassignable | Yes | Yes | No |
| Redeclarable in same scope | Yes | No | No |
| Attaches to global object | Yes (at top level) | No | No |
| Introduced in | ES1 (1997) | ES2015 (ES6) | ES2015 (ES6) |
| Temporal Dead Zone | No | Yes | Yes |
| Recommended for new code | No | When reassignment needed | Default choice |
What is var?
var is the original variable declaration keyword in JavaScript, predating the ES2015 specification by nearly two decades. It is function-scoped: a var declared anywhere inside a function is visible throughout that entire function, regardless of any nested blocks. At the top level of a script (outside any function), it becomes a property of the global object — window in browsers and global in Node.js.
var declarations are hoisted to the top of their scope and automatically initialised as undefined. This means you can reference a var variable before the line where it is written without throwing an error — you simply get undefined back, which is a common source of subtle bugs.
// Hoisting with var
console.log(count); // undefined — no error
var count = 5;
console.log(count); // 5
// var leaks out of blocks
if (true) {'{'}
var leaked = 'oops';
{'}'}
console.log(leaked); // 'oops' — still accessible What is let?
Introduced in ES2015, let declares a block-scoped variable. The variable only exists within the nearest enclosing block — typically a pair of curly braces. This makes behaviour far more intuitive: a loop counter declared with let does not leak into the surrounding function.
let is hoisted but is not initialised, leaving it in the Temporal Dead Zone (TDZ) from the start of the block until the declaration line is reached. Accessing the variable before that line throws a ReferenceError, making bugs visible rather than silently returning undefined.
// Block scoping with let
for (let i = 0; i < 3; i++) {'{'}
setTimeout(() => console.log(i), 100);
{'}'}
// Logs: 0, 1, 2 (each iteration gets its own binding)
// TDZ in action
console.log(name); // ReferenceError — in TDZ
let name = 'Alice'; What is const?
Also introduced in ES2015, const behaves identically to let in terms of block scope and the Temporal Dead Zone. The sole difference is that a const binding cannot be reassigned after initialisation — attempting to do so throws a TypeError at runtime (or a compile-time error in TypeScript).
A common misconception is that const makes values immutable. It does not. It freezes the binding — the reference stored in the variable — not the underlying object or array. Properties of a const object can still be added, changed, or deleted.
const config = {'{'} debug: false {'}'};
config.debug = true; // allowed — mutating the object
config = {'{'}{'}'}; // TypeError — reassigning the binding
const nums = [1, 2, 3];
nums.push(4); // allowed — mutating the array
nums = []; // TypeError
// Deep freeze requires extra work:
const frozen = Object.freeze({'{'} host: 'localhost' {'}'});
frozen.host = 'prod'; // silently ignored in sloppy mode
// TypeError in strict mode Key Differences in Practice
Scope and the loop closure problem
The classic JavaScript interview problem involves a for loop with asynchronous callbacks. With var, all callbacks share one variable. With let, each iteration creates a fresh binding — the problem disappears entirely without needing an IIFE workaround.
Global pollution
Scripts loaded via <script> tags share the global scope. A top-level var declaration adds a property to window, making name collisions between libraries a real risk. let and const do not pollute the global object, even at the top level of a script — in a module context they are completely isolated.
Redeclaration
var allows you to declare the same variable name multiple times in the same scope without error — the second declaration is silently ignored. let and const throw a SyntaxError if you attempt to redeclare the same name in the same scope, which catches copy-paste mistakes early.
How Engineers Talk About let, const, and var
These are phrases you will hear in code reviews, stand-ups, and technical interviews at English-speaking companies.
- "Just use
constby default and reach forletonly when you genuinely need to reassign." A common code-review recommendation that signals awareness of modern best practices. - "This closure captures the
var— that's why all your callbacks fire with the same value." Diagnosing the classic loop-plus-async bug during a debugging session. - "The TDZ is biting you here — you're reading the variable before its declaration." Explaining a ReferenceError caused by accessing a
letorconstbinding too early. - "
constdoesn't mean immutable — it just means you can't rebind the variable." Clarifying a widespread misconception during a code review or onboarding session. - "We have the
no-varESLint rule enabled, so please migrate this toletorconst." A pull-request comment asking a contributor to update legacy code to meet the project's linting standards. - "Prefer
consthere —prefer-constwill flag this in CI anyway." Pointing out that the ESLint rule will fail the build if aletis never reassigned. - "
varleaks out of theifblock — that's a function-scoping thing." Explaining why a variable declared inside anifstatement is still accessible outside it. - "We use
Object.freeze()on top ofconstfor config objects we truly never want mutated." Describing a deliberate pattern to enforce actual immutability, not just binding stability.
Decision Guide — Which Keyword to Use
- Default to
const. Most bindings in well-written JavaScript do not need reassignment. Usingconstsignals intent clearly and prevents accidental reassignment. It is the idiomatic choice in modern codebases and is enforced by ESLint'sprefer-construle. - Use
letwhen you must reassign. Loop counters (for (let i = 0; ...)), accumulator variables, and state that changes during execution are natural candidates. Keep the scope as narrow as possible. - Avoid
varin new code. Its function-scoping and hoisting quirks add cognitive overhead with no compensating benefit. Addno-varto your ESLint config to enforce this across the project. - When working with objects and arrays,
constdoes not prevent mutation. If you need deep immutability — for example, a shared configuration object — combineconstwithObject.freeze(), use TypeScript'sreadonlymodifier, or reach for a library such as Immer. - In legacy code, leave
varalone unless refactoring. Blindly replacingvarwithlet/constin untested legacy code can introduce subtle behaviour changes due to scope differences. Refactor deliberately, with tests in place.
Relevant ESLint Rules
These rules appear in most professional JavaScript/TypeScript projects:
// .eslintrc or eslint.config.js
{"{"}
"rules": {"{"}
"no-var": "error", // disallow var entirely
"prefer-const": "error", // require const when let is never reassigned
"no-use-before-define": "error", // catches TDZ access and hoisting bugs
"no-inner-declarations": "error" // disallows var inside blocks
{"}"}
{"}"}
Popular configurations — Airbnb, StandardJS, and eslint:recommended extended with @typescript-eslint — enable no-var and prefer-const by default. These rules are effectively zero-cost: they run at build time and catch entire categories of bugs before code reaches production.
Related Comparisons
If you are exploring JavaScript and TypeScript concepts further, these comparisons cover closely related decisions:
- TypeScript vs JavaScript — How TypeScript's
readonlykeyword, type inference, and strict null checks complement (and often replace) the discipline enforced byconstalone. - git merge vs rebase — Another foundational decision where understanding the underlying model (like scope rules in JS) prevents entire categories of mistakes.
- SQL vs NoSQL — When your JavaScript application's data layer needs the same kind of deliberate, upfront choice.
What is the main difference between let and var?
The key difference is scope. var is function-scoped and hoisted to the top of its containing function (or globally if declared outside a function). let is block-scoped, meaning it only exists within the nearest set of curly braces — an if block, a loop, or a function body. This makes let far more predictable in larger codebases.
Does const make an object immutable?
No. const creates a binding that cannot be reassigned — the variable always points to the same value in memory. However, if that value is an object or array, its properties and elements can still be mutated freely. To deeply freeze an object you need Object.freeze(), though that only covers one level. For true deep immutability, libraries such as Immer or TypeScript's readonly types are commonly used.
What is the Temporal Dead Zone (TDZ)?
The TDZ is the period between the start of a block scope and the line where a let or const variable is declared. If you try to access the variable during this period, JavaScript throws a ReferenceError. This behaviour is intentional — it prevents the confusing situation var creates where a variable technically exists but holds undefined before its declaration line.
Is var ever appropriate in modern JavaScript?
Rarely. Some engineers encounter var in legacy codebases, browser polyfills, or very old tutorials, so understanding it is important. For new code, the community consensus is to use const by default and let when reassignment is genuinely needed. ESLint's no-var rule can enforce this automatically across a project.
Why does var inside a for loop cause problems?
Because var is function-scoped, the loop variable is shared across all iterations. A classic bug occurs when closures inside the loop (for example, setTimeout callbacks or event listeners) all capture the same variable, which by the time they execute holds the final loop value. Using let creates a new binding per iteration, so each closure captures its own copy of the variable.
Can I use let or const at the top level of a module?
Yes. In ES modules, top-level let and const are scoped to the module — they do not become properties of the global object. This is a significant improvement over var, which at the global scope attaches itself to window (in browsers) or global (in Node.js), risking name collisions across scripts.
What ESLint rules enforce best practices for variable declarations?
The most commonly used rules are: no-var (disallows var entirely), prefer-const (flags let bindings that are never reassigned), no-use-before-define (catches accidental TDZ access), and no-inner-declarations (prevents var declarations inside blocks). Most popular ESLint configs such as eslint:recommended or Airbnb enable these by default.