let vs var javascript: A Practical Comparison for Modern JavaScript
A thorough comparison of let and var in JavaScript, covering scope, hoisting, TDZ, redeclaration rules, and practical guidance for modern, production-grade code.

In modern JavaScript, the recommended approach is to favor let and avoid var. Let provides block scoping and behaves with the temporal dead zone, reducing surprises, while var is function-scoped and can be redeclared. For legacy code or broad browser support, var may still appear, but new code should rely on let.
Core Concepts of let vs var javascript
In modern JavaScript, understanding the difference between let and var is crucial for writing predictable code. The keyword let introduces a block-scoped binding, while var creates a binding at function scope. This distinction affects hoisting, redeclaration, and the temporal dead zone (TDZ). According to JavaScripting, adopting block scope by default reduces variable leakage and makes refactoring safer in teams. When you declare let inside a loop or a block, each new block gets its own binding, preventing a shared binding from leaking across iterations.
Consider these contrasts:
- Hoisting and initialization:
- var declarations are hoisted and initialized to undefined.
- let declarations are hoisted but not initialized until the code executes the binding, which creates the TDZ.
- Redeclaration:
- var can be redeclared in the same scope.
- let cannot be redeclared in the same scope, which helps catch mistakes early.
Short examples:
function demoVar() {
var a = 1;
var a = 2; // allowed
return a; // 2
}
function demoLet() {
let b = 1;
// let b = 2; // SyntaxError: Identifier 'b' has already been declared
return b;
}Hoisting, TDZ, and how they affect let vs var javascript
Hoisting is a JavaScript behavior where declarations are moved to the top of their scope before execution. With var, the declaration is hoisted and initialized to undefined, which can mask bugs. With let, the binding is hoisted but not initialized, introducing a temporal dead zone (TDZ) that prevents access until the binding is created. Accessing a let-bound variable in the TDZ results in a ReferenceError, which helps surface mistakes early.
Examples:
- var x = 5; console.log(x); // 5
- console.log(y); var y = 10; // undefined (hoisted, then assigned later) or ReferenceError in strict mode
- console.log(z); let z = 3; // ReferenceError: Cannot access 'z' before initialization
Block scope vs function scope: practical impact
The most visible difference is the scope boundary. let is block-scoped; var is function-scoped. In loops and conditionals, this changes behavior dramatically. For example, in a for loop, using let inside the loop header creates a new binding per iteration, so closures capture the correct value. With var, the same binding is shared across iterations, leading to all closures seeing the final value.
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
} // logs 0, 1, 2
for (var j = 0; j < 3; j++) {
setTimeout(() => console.log(j), 0);
}
// logs 3, 3, 3Redeclaration and reassignment rules
Redeclaration rules differ between let and var. Var can be redeclared within the same scope, which can hide bugs introduced by repeated declarations. Let forbids redeclaration in the same scope, which helps catch mistakes early. Both variables are reusable and reassignable, but the guardrails differ.
function redeclare() {
var x = 1;
var x = 2; // OK
return x; // 2
}
function noRedeclare() {
let y = 5;
// let y = 6; // SyntaxError: Identifier 'y' has already been declared
y = 6; // allowed (reassignment)
return y;
}When to use let in modern codebases
In new projects and modules, let should be your default choice for variables that will change over time. It enforces block scope, reducing accidental leakage and making functions more predictable. For values that never change, prefer const. Together, let and const provide a modern baseline aligned with eslint and other tooling. Use let for loop counters, temporary bindings inside blocks, and values that will be reassigned.
When var is still relevant
Var remains relevant only for legacy codebases, very old browsers, or scripts that need to preserve global scope behavior. In some polyfilled environments, var syntax may be easier to reason about if the team is working within a codebase that predates ES6. When maintaining or gradually upgrading legacy code, plan a staged refactor to replace var with let/const where possible, ensuring tests cover behavior changes.
Common pitfalls and how to avoid them
- Redeclaring with var in the same scope goes unnoticed until runtime. Avoid by using let and modern lint rules.
- Accessing a let binding before initialization triggers TDZ errors. Initialize early or restructure code to avoid premature access.
- Relying on function-scoped behavior of var in loops can cause closures to capture the final value. Prefer let in loop headers to capture per-iteration values.
- Mixing let and var in the same scope with a sloppy apprenticeship to hoisting can cause confusion. Favor a consistent style guide.
Refactoring old code: migrating from var to let
A pragmatic migration approach:
- Run tests to identify var-related edge cases.
- Replace var with let in small, well-scoped modules first.
- Update code comments and linting rules to reflect the new standard.
- Audit for redeclarations and hoisting-related bugs; adjust as needed.
- Use ESLint rules (no-var, prefer-const) to automate future changes.
- Validate performance and behavior with real-user tests.
Best practices for teams and tooling
- Adopt a single source of truth: a team-wide style guide that defaults to let/const.
- Enable strict mode and lint rules that catch redeclarations, TDZ violations, and hoisting surprises.
- Write clear, scoped code with meaningful variable names to reduce confusion around scope.
- Use const by default; switch to let only when reassignment is required.
- Periodically review legacy code to plan scalable migrations.
Comparison
| Feature | let | var |
|---|---|---|
| Scope | Block scope (let) | Function scope (var) |
| Hoisting | Accessible after TDZ within a block | Hoisted and initialized to undefined |
| Redeclaration | Cannot redeclare in the same scope | Can be redeclared in the same scope |
| Temporal Dead Zone (TDZ) | Yes (error if accessed before binding) | No TDZ |
| Best for | Block-scoped, safer modern code | Legacy code and broader compatibility |
Benefits
- Reduces accidental leakage by enforcing block scope
- Encourages clearer, more maintainable code with per-block bindings
- Works well with modern tooling and lint rules
- Promotes safer patterns when used with const for immutables
The Bad
- Var remains in legacy code and some old environments
- Redeclaration in older code can hide bugs if not careful
Let is the default, modern choice; var should be reserved for legacy compatibility
Choose let for new code to enforce block scope and TDZ safety. Reserve var for legacy scripts or compatibility scenarios where updating legacy code isn't feasible. Integrate with linting to enforce the standard.
Questions & Answers
What is the primary difference between let and var in JavaScript?
The primary difference is scope: let is block-scoped, while var is function-scoped. Let also has a temporal dead zone, which prevents access before initialization, reducing surprises in code.
The main difference is scope: let is block-scoped, var is function-scoped, with let also having a timing rule that prevents early access.
What is the temporal dead zone (TDZ) for let?
TDZ is the period during which a let binding exists but is not yet initialized. Accessing the binding in this window throws a ReferenceError, helping catch mistakes early.
TDZ means you can’t use a let-bound variable before you’ve assigned it.
Can I redeclare a let variable in the same scope?
No. Let declarations cannot be redeclared in the same scope, which helps catch accidental duplicates and potential bugs.
You can’t redeclare a let variable in the same scope.
Is var hoisted in JavaScript?
Yes. Var declarations are hoisted to the top of their scope and initialized with undefined, which can hide bugs if you expect execution order.
Var is hoisted and initialized to undefined, which can be tricky.
When should I still use var?
Use var only when maintaining legacy code or ensuring compatibility with very old browsers that don’t support ES6 features.
Only for legacy code or very old environments.
How does let interact with closures inside loops?
Using let in loop headers creates a fresh binding per iteration, so closures captured inside the loop see the correct value per iteration.
Let in loops gives each iteration its own binding so closures see the right value.
What to Remember
- Prefer block-scoped let by default
- Use const when values are not reassigned
- Avoid redeclaration with let
- Plan gradual migration from var to let in legacy code
- Leverage lint rules to enforce modern scoping practices
