Skip to main content

Differences between var, let and const

var, let, and const - the three keywords for declaring variables in JavaScript, each with different scoping rules, hoisting behavior, and constraints on reassignment.

Theory

TL;DR

  • var is function-scoped; let and const are block-scoped
  • All three hoist, but var initializes to undefined while let/const stay in the temporal dead zone (TDZ) until the declaration line
  • const blocks reassignment; var and let allow it
  • Default choice: const. Use let only when reassignment is needed. Avoid var in new code
  • Analogy: var is a whiteboard in the hallway (visible from anywhere in the function); let/const are notes pinned inside a specific room (visible only in that block)

Quick example

javascript
console.log(varX); // undefined (hoisted, initialized to undefined) var varX = 1; console.log(letX); // ReferenceError: Cannot access 'letX' before initialization let letX = 2; for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 0); // logs 3, 3, 3 (shared var) } for (let j = 0; j < 3; j++) { setTimeout(() => console.log(j), 0); // logs 0, 1, 2 (fresh binding per iteration) }

var in a loop shares one i across all iterations. let creates a fresh binding per iteration, so each closure captures its own value.

Key difference

var ignores if, for, and while blocks completely. It belongs to the nearest function or to global scope. let and const respect curly braces, so a variable declared inside a block stays there. That one rule explains most bugs that come from var leaking into outer scopes.

When to use

  • Config values, API keys, imported modules → const
  • Loop counters that need incrementing, mutable locals → let
  • Any value that gets reassigned → let
  • Any value that never changes → const (including objects and arrays)
  • Maintaining existing legacy codebases only → var

Comparison table

Featurevarletconst
ScopeFunction / globalBlockBlock
HoistingYes, initializes to undefinedYes, but stays in TDZYes, but stays in TDZ
RedeclarationAllowedNot allowedNot allowed
ReassignmentAllowedAllowedNot allowed
Initialization requiredNoNoYes
When to useLegacy code onlyCounters, mutable varsDefault choice

How the compiler handles this

In V8, the engine creates a LexicalEnvironment per scope during compilation. var bindings go into the function's VariableEnvironment and are immediately set to undefined, which is why reading a var before its declaration line gives undefined instead of an error. let and const also enter the LexicalEnvironment at compile time, but their binding sits in an uninitialized state. Reading them before the declaration line triggers a ReferenceError. That window between entering scope and the declaration line is the TDZ.

Common mistakes

Mistake 1: var in loops with async callbacks

javascript
for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 0); // logs 3, 3, 3 }

All callbacks share one i because var is function-scoped. By the time they fire, the loop is done and i is 3. This is a classic interview trap. Fix: use let.

Mistake 2: Thinking const means immutable

javascript
const arr = [1, 2]; arr.push(3); // works fine, arr is now [1, 2, 3] arr = [4, 5]; // TypeError: Assignment to constant variable

const prevents reassignment of the variable binding itself, not mutation of the value it holds. For a truly immutable array or object, use Object.freeze().

Mistake 3: Reading let or const before the declaration

javascript
console.log(x); // ReferenceError, not undefined let x = 5;

This does not silently return undefined like var would. The variable is in TDZ. The engine knows it exists but refuses access until the declaration line runs.

Real-world usage

  • React: const for hooks (const [count, setCount] = useState(0)), component definitions, all imports
  • Node.js / Express: const app = express(), const router = express.Router()
  • Redux: const FETCH_USER = 'FETCH_USER' for action type constants
  • Loops with mutable counters: for (let i = 0; i < arr.length; i++)

Follow-up questions

Q: What is the temporal dead zone?
A: The TDZ is the period between a let or const variable entering scope (at compile time) and reaching its declaration line. Any access in that window throws a ReferenceError. var has no TDZ because it initializes to undefined right away.

Q: Can you mutate an object declared with const?
A: Yes. const obj = {}; obj.name = 'Alice' works fine. const blocks reassignment only: obj = {} throws a TypeError. Use Object.freeze(obj) if you need the object itself to be immutable.

Q: Why does for (var i = 0; ...) with setTimeout always log the final value of i?
A: All iterations share one i because var is function-scoped. Closures capture a reference to that shared variable, not a snapshot. When the callbacks fire, the loop is already done and i is at its final value. Switching to let gives each iteration its own binding.

Q: Does top-level var attach to window in Node.js?
A: No. Browsers attach top-level var to window. In Node.js every file runs inside a module wrapper function, so top-level var stays local to that module.

Q: Walk through what V8 does when let is shadowed in a nested block.
A: V8 creates a new LexicalEnvironment for each block during compilation. The outer let binding goes into the outer env. When execution enters the inner block, V8 creates a second LexicalEnvironment chained to the outer one. The inner let goes there and shadows the outer binding. Variable lookup walks up the chain, so inner code finds its own binding first. Both bindings start in TDZ until their declaration lines execute.

Examples

Scope leak: var vs let

javascript
function checkScope() { if (true) { var x = 10; // leaks out to function scope let y = 20; // stays inside this block } console.log(x); // 10, var leaked out console.log(y); // ReferenceError, let did not leak } checkScope();

var does not respect the if block boundary. let does. One variable escapes the block, the other stays put. That difference is the entire reason let exists.

React component with proper declarations

javascript
function UserList({ users }) { const [filteredUsers, setFilteredUsers] = useState(users); return ( <ul> {filteredUsers.map(user => { const userId = user.id; // fresh const per iteration block, never reassigned return <li key={userId}>{user.name}</li>; })} </ul> ); }

Everything here is const because nothing gets reassigned. setFilteredUsers is the function you call to schedule a state update, not a value you overwrite directly. userId is a new block-scoped constant on every iteration.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?