Why You Should Stop Using var in JavaScript
Discover the real reasons why var causes bugs in JavaScript and learn to replace it with let and const. This guide covers scope leaks, hoisting surprises, closure traps, and practical migration strategies.
If you have been writing JavaScript for any amount of time, you have almost certainly used var. It was the only way to declare variables for the first 20 years of the language. But since ES6 introduced let and const in 2015, var has become a liability. It introduces bugs that are hard to detect, hard to debug, and completely preventable. This article explains exactly why var is problematic, demonstrates the real-world bugs it causes, and shows you how to migrate your code safely.
This is not about style preferences or following trends. Every issue described in this article represents a class of real bugs that var either enables or hides. By the end, you will understand why every major JavaScript style guide, linter configuration, and framework template discourages or outright bans var.
The Core Problem: Function Scope Instead of Block Scope
The single biggest issue with var is that it uses function scope instead of block scope. Variables declared with var ignore block boundaries like if, for, while, and switch. They are only bounded by the nearest enclosing function.
function calculateDiscount(price, isMember) {
if (isMember) {
var discount = 0.2;
var message = "Member discount applied";
}
// These variables leak out of the if-block
console.log(discount); // 0.2 (or undefined if isMember is false)
console.log(message); // "Member discount applied" (or undefined)
// With let, this would throw ReferenceError, catching the bug immediately
}In languages like Java, C#, Python, and nearly every other programming language, variables declared inside a block are not accessible outside it. var breaks this expectation. Every developer who switches to JavaScript from another language gets bitten by this.
// Another common scope leak: loop variables
function processItems(items) {
for (var i = 0; i < items.length; i++) {
var item = items[i];
// process item...
}
// Both i and item are accessible here
console.log(i); // items.length (leaked from the loop)
console.log(item); // last item in the array (leaked from the loop)
}Block Scope is the Industry Standard
Almost every modern programming language uses block scope. JavaScript's var was an exception driven by the language's original 10-day development timeline. let and const corrected this to match what developers expect.
Hoisting: The Silent Bug Factory
var declarations are hoisted to the top of their function scope and initialized with undefined. This means you can access a var variable before its declaration line without getting an error, which hides bugs.
function getUserGreeting(user) {
// No error here, even though 'greeting' is declared 10 lines below
console.log(greeting); // undefined (silently wrong)
if (user.isPremium) {
var greeting = `Welcome back, ${user.name}!`;
} else {
var greeting = `Hello, ${user.name}!`;
}
return greeting;
}Compare this to let, which throws a clear ReferenceError if you try to access it before declaration:
function getUserGreeting(user) {
// console.log(greeting); // ReferenceError: Cannot access 'greeting'
// This error immediately tells you something is wrong
let greeting;
if (user.isPremium) {
greeting = `Welcome back, ${user.name}!`;
} else {
greeting = `Hello, ${user.name}!`;
}
return greeting;
}The var version silently produces undefined instead of throwing an error. In a large codebase, this kind of silent failure can propagate far before anyone notices the incorrect value.
Same-Scope Redeclaration: Accidental Overwrites
var allows you to declare the same variable name multiple times in the same scope without any warning. This is a significant source of bugs in longer functions.
function handleFormSubmit(formData) {
var status = "pending";
// ... 50 lines of validation code ...
var status = validateEmail(formData.email); // Oops! Accidentally redeclared
// ... 30 more lines ...
var status = "submitted"; // Redeclared again!
// The original 'pending' status was silently overwritten twice
console.log(status); // "submitted"
}With let or const, the engine catches this mistake immediately:
let status = "pending";
// let status = "submitted"; // SyntaxError: Identifier 'status' has already been declared| Declaration | Redeclare in Same Scope | What Happens |
|---|---|---|
var status = "a"; var status = "b"; | Allowed | Silently overwrites; no error |
let status = "a"; let status = "b"; | Not allowed | SyntaxError at parse time |
const status = "a"; const status = "b"; | Not allowed | SyntaxError at parse time |
The Classic Loop Closure Bug
This is the most widely cited var bug, and it surfaces in any code that combines loops with callbacks, event handlers, or timers.
// BUG: All buttons log "Button 5" because var is function-scoped
function setupButtons() {
const buttons = document.querySelectorAll(".action-btn");
for (var i = 0; i < buttons.length; i++) {
buttons[i].addEventListener("click", function() {
console.log(`Clicked button ${i}`); // Always logs the final value of i
});
}
}
// FIX: Each iteration gets its own i with let
function setupButtons() {
const buttons = document.querySelectorAll(".action-btn");
for (let i = 0; i < buttons.length; i++) {
buttons[i].addEventListener("click", function() {
console.log(`Clicked button ${i}`); // Correctly logs 0, 1, 2, 3, 4
});
}
}The root cause: var creates one i for the entire function. By the time any click handler executes, the loop has finished and i holds its final value. let creates a new i for each loop iteration, so each handler captures its own copy.
// The same bug with setTimeout
console.log("--- var version ---");
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Logs: 3, 3, 3
console.log("--- let version ---");
for (let j = 0; j < 3; j++) {
setTimeout(() => console.log(j), 100);
}
// Logs: 0, 1, 2Before let existed, developers had to use an IIFE (Immediately Invoked Function Expression) to work around this:
// The old workaround (ugly but necessary before ES6)
for (var i = 0; i < 3; i++) {
(function(capturedI) {
setTimeout(() => console.log(capturedI), 100);
})(i);
}
// Logs: 0, 1, 2The IIFE workaround is error-prone and harder to read. let solves the problem at the language level.
Global Namespace Pollution
var declarations at the global level attach properties to the window object. This pollutes the global namespace and risks collisions with browser APIs, third-party libraries, or other scripts on the page.
// In a browser's global scope
var apiKey = "abc123";
console.log(window.apiKey); // "abc123" (exposed on window!)
let secret = "xyz789";
console.log(window.secret); // undefined (safely contained)
const config = { debug: false };
console.log(window.config); // undefined (safely contained)| Declaration Location | var | let / const |
|---|---|---|
| Inside a function | Function-scoped | Block-scoped |
| At the global level | Attaches to window | Does NOT attach to window |
| In a module | Module-scoped | Module-scoped |
In a for loop | Function-scoped (leaks) | Loop-block-scoped (contained) |
This global pollution issue was so serious that the JavaScript module system was partly designed to avoid it. If you use ES modules (import/export), top-level var is module-scoped (not global), but many scripts still run in the global scope.
What Every Style Guide Says
Every major JavaScript style guide has taken a clear position on var:
Airbnb JavaScript Style Guide: "Use const for all of your references; avoid using var. If you must reassign references, use let instead of var."
Google JavaScript Style Guide: "Declare all local variables with either const or let. Use const by default, unless a variable needs to be reassigned. The var keyword must not be used."
StandardJS: Prefers const and let over var in all cases.
ESLint default recommended rules: The no-var rule and prefer-const rule are both widely enabled.
// ESLint configuration to enforce no-var
// eslint.config.mjs
export default [
{
rules: {
"no-var": "error", // Disallow var declarations
"prefer-const": "error", // Prefer const when variable is never reassigned
}
}
];Migration Strategy: Replacing var with let and const
Migrating existing code from var to let/const is usually straightforward, but there are edge cases.
Step 1: Run the Linter
# Install ESLint if not already installed
npm install eslint --save-dev
# Run the lint fix for no-var
npx eslint --fix --rule 'no-var: error' src/Step 2: Review Each Replacement
Not every var can be mechanically replaced. Check for these patterns:
// SAFE: Simple replacement
var name = "Alice"; // becomes: const name = "Alice";
var count = 0; count++; // becomes: let count = 0; count++;
// CAUTION: var in a switch statement
switch (action) {
case "add":
var result = x + y; // This var is function-scoped
break;
case "subtract":
var result = x - y; // Redeclares! Works with var, SyntaxError with let
break;
}
// FIX: Declare once before the switch
let result;
switch (action) {
case "add":
result = x + y;
break;
case "subtract":
result = x - y;
break;
}Step 3: Special Cases
// Global var used for cross-script communication
var GLOBAL_CONFIG = { debug: true }; // Other scripts check window.GLOBAL_CONFIG
// If you need window attachment, be explicit instead:
window.GLOBAL_CONFIG = { debug: true };
const localConfig = window.GLOBAL_CONFIG; // Use const locallyMigration Is Low Risk
Replacing var with let or const rarely introduces new bugs. It usually reveals existing bugs that var was hiding. If your code breaks after replacing var, the underlying problem was already there.
Best Practices
Start every declaration with const. Only switch to let when the code requires reassignment. This makes every reassignment intentional and visible to code reviewers.
Enable no-var globally in your linter. This single rule prevents anyone on your team from accidentally introducing var declarations. Set it to "error", not "warn".
Use block-scoped declarations to communicate intent. A const says "this binding never changes." A let says "this binding will change." var says nothing useful about intent.
Declare variables close to where they are used. With block scope, you can declare variables inside if, for, and other blocks. This keeps them close to their usage and reduces the mental footprint of the code.
Fix var declarations during code reviews. Any var in a code review is an automatic comment. Over time, this gradually migrates the codebase without requiring a dedicated refactoring sprint.
Common Mistakes and How to Avoid Them
Replacing var without checking for redeclarations. If a function uses var x multiple times, replacing all of them with let x will cause SyntaxError. Declare once at the top and assign in each location instead.
Assuming const means immutable. const prevents reassignment, but objects and arrays declared with const can still have their contents modified. If you need true immutability, use Object.freeze().
Using let everywhere "to be safe." Excessive let usage when const would work hides information. Readers have to manually verify whether a let variable is ever reassigned. const communicates this automatically.
Not configuring the linter. Without no-var enforced in your linting setup, var will inevitably sneak back into the codebase through habit or copy-pasted Stack Overflow answers.
Forgetting the loop closure fix. Even experienced developers occasionally forget that var in a for loop with async callbacks produces bugs. Make let your default for all loop variables.
Next Steps
Learn complete variable declaration syntax
Read our comprehensive JS Variables Guide to understand every aspect of variable declaration, initialization, and usage.
Master [variable naming](/tutorials/programming-languages/javascript/javascript-variable-naming-conventions-rules) conventions
Good variable names are just as important as using the right keyword. Learn the JavaScript variable naming conventions and rules that professional codebases follow.
Understand scope in depth
Scope is the reason var causes bugs. Read about global vs local variables in JavaScript to deepen your understanding of how scope works.
Configure ESLint for your project
Set up ESLint with no-var and prefer-const rules to automatically enforce modern variable declarations across your entire codebase.
Rune AI
Key Insights
- No modern use case for var:
letandconsthandle every scenario more safely thanvar - Function scope is the root cause:
varignores block boundaries, causing variable leaks thatlet/constprevent entirely - Hoisting hides bugs:
varreturnsundefinedbefore declaration instead of throwing aReferenceErrorlikelet/const - Linters enforce the rule: Enable ESLint's
no-varandprefer-construles to preventvarfrom entering your codebase - Migration reveals existing bugs: Replacing
varrarely creates new issues; it exposes problems that were already present but hidden
Frequently Asked Questions
Is var officially deprecated in JavaScript?
Will replacing var with let break my code?
Should I refactor an entire legacy codebase to remove var?
Is there any case where var is better than let or const?
Do TypeScript and modern frameworks support var?
Conclusion
var was the right tool for JavaScript in 1995, but it is the wrong tool in 2026. Its function scope causes variables to leak beyond their intended boundaries. Its hoisting behavior masks bugs by silently returning undefined instead of throwing errors. Its same-scope redeclaration allows accidental variable overwrites. Its global-level behavior pollutes the window object. Every one of these issues is fixed by let and const. The migration path is simple, the tooling is mature, and there is zero performance cost. Stop using var today.
More in this topic
OffscreenCanvas API in JS for UI Performance
Master the OffscreenCanvas API to offload rendering from the main thread. Covers worker-based 2D and WebGL rendering, animation loops inside workers, bitmap transfer, double buffering, chart rendering pipelines, image processing, and performance measurement strategies.
Advanced Web Workers for High Performance JS
Master Web Workers for truly parallel JavaScript execution. Covers dedicated and shared workers, structured cloning, transferable objects, SharedArrayBuffer with Atomics, worker pools, task scheduling, Comlink RPC patterns, module workers, and performance profiling strategies.
JavaScript Macros and Abstract Code Generation
Master JavaScript code generation techniques for compile-time and runtime metaprogramming. Covers AST manipulation, Babel plugin authorship, tagged template literals as macros, code generation pipelines, source-to-source transformation, compile-time evaluation, and safe eval alternatives.