Let vs Var in JavaScript: A Practical Guide

Explore why let is preferred over var in JavaScript, with practical examples, scope rules, hoisting, and migration tips to write safer, more maintainable code.

JavaScripting
JavaScripting Team
·5 min read
Quick AnswerDefinition

Let and var differ in scope, hoisting, and the temporal dead zone. In modern JavaScript, let helps you prevent global leakage and accidental re-declarations. The takeaway: prefer let for block-scoped variables and reserve var for legacy code or function-scoped patterns.

Block scope and for loops: let vs var

Let and var behave differently inside loops. With var, the loop variable leaks outside the loop. With let, each iteration gets a fresh binding, avoiding surprises in closures.

JavaScript
for (var i = 0; i < 3; i++) { setTimeout(() => console.log(i), 0); } for (let j = 0; j < 3; j++) { setTimeout(() => console.log(j), 0); }
  • The first loop prints 3 three times, showing var's function scope. The second prints 0, 1, 2 because let has block scope per iteration.

Takeaway: Use let for loop indices to avoid unexpected sharing between iterations.

Temporal Dead Zone and initialization

Accessing a let variable before it is initialized triggers a ReferenceError due to the temporal dead zone. In contrast, accessing a var before initialization yields undefined. This difference helps catch mistakes early and prevents unintended behavior.

JavaScript
console.log(a); // ReferenceError: Cannot access 'a' before initialization let a = 5;
JavaScript
console.log(b); // undefined var b = 5;

Why it matters: TDZ enforces deliberate initialization and reduces bugs caused by reading uninitialized variables.

Hoisting myths and how it affects readability

JavaScript hoists declarations to the top of their scope, but only var declarations are initialized. Let declarations are hoisted but not initialized, which contributes to the temporal dead zone. This behavior makes code easier to reason about when you use let throughout your codebase.

JavaScript
console.log(x); // ReferenceError in TDZ let x = 10;
JavaScript
console.log(y); // undefined until initialization var y = 10;

Takeaway: Treat var hoisting as a historical artifact; prefer let to avoid surprises.

Shadowing and redeclarations in blocks vs functions

Let supports block scope, so you can declare the same variable name in nested blocks without clobbering outer bindings. Var ignores block boundaries, which can lead to subtle bugs. See the contrast:

JavaScript
let a = 'outer'; if (true) { let a = 'inner'; console.log(a); // 'inner' } console.log(a); // 'outer'
JavaScript
var b = 'outer'; if (true) { var b = 'inner'; console.log(b); // 'inner' } console.log(b); // 'inner' (same binding)

Implication: Avoid var if you want predictable scope and reduce accidental overwrites.

Practical migration patterns: refactoring var to let in modules

Migrating large codebases requires a plan. Start with a targeted module and convert var to let where the variable is not intended to leak beyond its block. Use tests to verify behavior. Here is a typical before/after pattern:

JavaScript
// Before for (var i = 0; i < items.length; i++) { console.log(i, items[i]); } // After for (let i = 0; i < items.length; i++) { console.log(i, items[i]); }
JSON
/* ESLint config excerpt to enforce no-var */ { "rules": { "no-var": "error", "prefer-const": "error" } }

Approach: Migrate iterators, closures, and global declarations first; keep edge-case var only where necessary.

Closures and loops: using let fixes captured values in callbacks

Closures in loops behave differently when you use var. The classic pitfall is that all callbacks capture the same loop variable. Replacing with let binds a new value per iteration, fixing the pattern:

JavaScript
var callbacks = []; for (var i = 0; i < 3; i++) { callbacks.push(function(){ return i; }); } console.log(callbacks.map(f => f())); // [3, 3, 3]
JavaScript
var callbacksLet = []; for (let i = 0; i < 3; i++) { callbacksLet.push(function(){ return i; }); } console.log(callbacksLet.map(f => f())); // [0, 1, 2]

Lesson: Use let to create fresh bindings for each iteration inside loops that spawn asynchronous work.

Tooling and linting: enforcing block-scoped declarations

A robust linting setup helps catch var usage and enforces best practices. Configure ESLint to reject var and prefer let/const:

JavaScript
// eslint config (snippet) module.exports = { "env": { "es6": true }, "rules": { "no-var": "error", "prefer-const": "error", "no-unused-vars": "warn" } };

Next steps: Integrate with your CI to ensure consistency across teams.

Browser support and transpilation: ES6+ required

Let was introduced in ES6 (ES2015). If you need to support older environments, use a transpiler like Babel with @babel/preset-env to compile to a compatible target. This ensures modern code using let runs in legacy browsers.

JS
// Babel config example { "presets": ["@babel/preset-env"] }

Tip: Always test in target environments when migrating from var to let.

Best practices and quick guidelines for teams

In new codebases, adopt let (and const for immutable bindings) as the default. Avoid using var unless you have a compelling reason tied to legacy support. Document decisions and create lint rules to prevent regressions. The long-term payoff is fewer scope bugs, easier reasoning, and safer refactoring.

Steps

Estimated time: 15-30 minutes per module depending on size

  1. 1

    Audit your codebase for var usage

    Scan modules to identify var declarations and assess their scope and potential leakage.

    Tip: Use a static analysis tool to pinpoint risky var usage.
  2. 2

    Replace var with let where appropriate

    Refactor to block-scoped bindings and run tests to validate behavior.

    Tip: Leave var in global/module boundaries only if legacy behavior requires it.
  3. 3

    Run linting and tests

    Enable ESLint no-var rule and run unit tests to catch regressions.

    Tip: Configure lint to fail on var usage.
  4. 4

    Review closures and loops

    Ensure closures capture correct values using let where needed.

    Tip: Prefer let in loops to avoid stale bindings.
  5. 5

    Document the migration

    Add notes in project docs and commit messages to explain the change.

    Tip: Include before/after examples in docs.
Pro Tip: Prefer let for all newly written variables to enforce block scope.
Warning: Do not replace all var blindly; assess side effects in global scope and IIFEs.
Note: Use eslint with no-var and related rules to guide refactors.

Prerequisites

Required

Optional

Keyboard Shortcuts

ActionShortcut
Toggle line commentToggle comment on selected linesCtrl+/
Format documentAuto-format your JavaScript file+Alt+F
Go to definitionJump to function/variable definitionF12

Questions & Answers

What is the main difference between let and var?

Let provides block scope and TDZ safety, while var uses function scope and can hoist and redeclare in ways that cause surprises.

Let gives block scope, preventing unexpected leaks. Var is function-scoped and can be hoisted, which may cause surprising behavior.

Does let get hoisted like var?

Yes, both are hoisted in memory, but let is not initialized until the declaration, triggering the temporal dead zone if accessed early.

Yes, it's hoisted but not initialized, so you get a TDZ error if you use it before declaration.

Can I still use var in legacy code?

You technically can, but it's better to migrate gradually and isolate var usage, especially in new modules.

You can, but prefer let in new code and plan a gradual migration for legacy code.

Is there a performance difference between let and var?

No meaningful performance difference; the choice is about scope, correctness, and maintainability.

Performance isn't the reason to switch; it's about scope and safer code.

Are there cases where var is still appropriate?

In very old code, or when you rely on function-wide scoping in IIFEs, var might be used, but modern code should prefer let.

Only in legacy patterns; otherwise, use let.

How can I enforce using let across a codebase?

Use ESLint with no-var and related rules to enforce block-scoped declarations.

Configure ESLint to warn or fail on var usage.

What to Remember

  • Choose let for block scope when declaring variables
  • Avoid hoisting surprises by not using var
  • Refactor gradually with linting and tests
  • Document migration decisions for team consistency

Related Articles