The Old "var" Keyword in JavaScript
If you are learning JavaScript today, you will mostly use let and const to declare variables. But var has not disappeared. It lives on in millions of lines of legacy code, older tutorials, Stack Overflow answers, library source code, and interview questions. Understanding how var behaves, and specifically how it differs from let and const, is essential for reading, debugging, and maintaining real-world JavaScript.
This guide explains every quirk of var in detail: its function-level scoping, its hoisting behavior, its ability to be re-declared, and why developers invented elaborate workarounds like IIFEs to compensate for its shortcomings. By the end, you will understand exactly why let and const replaced var and how to safely migrate legacy code.
var Has No Block Scope (Only Function Scope)
The single most important difference between var and let/const is how they handle scope. While let and const are block-scoped (confined to the nearest {}), var is function-scoped (confined only to the nearest function). Any block that is not a function, such as if, for, while, or a standalone {}, is completely ignored by var.
var Leaks Out of if Blocks
if (true) {
var message = "Hello from inside the if block";
}
console.log(message); // "Hello from inside the if block"
The variable message is fully accessible outside the if block. Compare this with let:
if (true) {
let message = "Hello from inside the if block";
}
// console.log(message); // ReferenceError: message is not defined
var Leaks Out of for Loops
This is one of the most common sources of bugs:
for (var i = 0; i < 5; i++) {
// some work
}
console.log(i); // 5 ("i" escaped the loop!)
With let, the loop variable stays inside the loop:
for (let j = 0; j < 5; j++) {
// some work
}
// console.log(j); // ReferenceError: j is not defined
var Leaks Out of while, switch, and Standalone Blocks
while (false) {
var neverExecuted = "ghost";
}
console.log(neverExecuted); // undefined (declared but never assigned)
switch ("a") {
case "a":
var switchVar = 42;
break;
}
console.log(switchVar); // 42
{
var blockVar = "I escaped!";
}
console.log(blockVar); // "I escaped!"
In every case, var ignores the block boundaries entirely.
var IS Scoped to Functions
The one boundary var respects is the function. Variables declared with var inside a function cannot be accessed outside that function:
function example() {
var localVar = "I'm local to this function";
console.log(localVar); // "I'm local to this function"
}
example();
// console.log(localVar); // ReferenceError: localVar is not defined
This applies to nested blocks within functions too. The var variable belongs to the function, not to any inner block:
function process() {
if (true) {
for (var i = 0; i < 3; i++) {
var result = i * 2;
}
}
// Both "i" and "result" are accessible here
// because they belong to the function scope, not the if/for blocks
console.log(i); // 3
console.log(result); // 4
}
process();
// console.log(i); // ReferenceError (function boundary IS respected)
// console.log(result); // ReferenceError
Global var Becomes a Property of window
When var is used at the top level (outside any function), it creates a property on the global object (window in browsers):
var globalVar = "I'm global";
console.log(window.globalVar); // "I'm global" (in browsers)
let globalLet = "I'm also global";
console.log(window.globalLet); // undefined
This can lead to accidental conflicts with built-in global properties or third-party scripts.
var at the top level pollutes the global window object. This means your variables can collide with browser APIs, other scripts on the page, or third-party libraries. let and const at the top level do not create properties on window.
Side-by-Side Scope Comparison
function scopeDemo() {
// var is function-scoped
if (true) {
var x = 1;
}
console.log(x); // 1 (accessible outside the if block)
// let is block-scoped
if (true) {
let y = 2;
}
// console.log(y); // ReferenceError (y is confined to the if block)
// const is block-scoped
if (true) {
const z = 3;
}
// console.log(z); // ReferenceError (z is confined to the if block)
}
var Declarations Are Hoisted (But Not Initializations)
Hoisting means that var declarations are processed before any code in their scope runs. The JavaScript engine moves the declaration to the top of the function (or script), but the assignment stays where you wrote it.
Basic Hoisting Example
console.log(greeting); // undefined (not ReferenceError!)
var greeting = "Hello";
console.log(greeting); // "Hello"
The engine processes this as if you had written:
var greeting; // Declaration is hoisted to the top
console.log(greeting); // undefined (declared but not yet assigned)
greeting = "Hello"; // Assignment stays in place
console.log(greeting); // "Hello"
Notice that the value is not hoisted, only the declaration. The variable exists from the start of the scope, but its value is undefined until the assignment line executes.
Comparison with let and const
let and const are also "hoisted" in the sense that the engine knows about them, but they are placed in a Temporal Dead Zone (TDZ) from the start of the block until the declaration line. Accessing them before the declaration throws a ReferenceError:
// var: hoisted, initialized to undefined
console.log(a); // undefined
var a = 10;
// let: hoisted but in TDZ
// console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 20;
// const: hoisted but in TDZ
// console.log(c); // ReferenceError: Cannot access 'c' before initialization
const c = 30;
The let/const behavior is actually safer. Getting a ReferenceError immediately tells you something is wrong. Getting undefined silently from var can hide bugs that are hard to track down.
Hoisting Inside Functions
Hoisting happens at the top of the containing function, not the top of the script:
function example() {
console.log(x); // undefined (hoisted to the top of the function)
if (false) {
var x = 42; // This line NEVER executes, but the declaration is still hoisted
}
console.log(x); // undefined (the assignment never ran)
}
example();
The engine treats the above as:
function example() {
var x; // Hoisted from inside the if block to the function top
console.log(x); // undefined
if (false) {
x = 42; // Never executes
}
console.log(x); // undefined
}
This is a particularly tricky behavior. Even though the if (false) block never runs, the var x declaration inside it is still hoisted. The declaration is processed, but the assignment is not.
Hoisting Can Mask Errors
Consider this misleading code:
var name = "Alice";
function greet() {
console.log("Hello, " + name);
var name = "Bob"; // Oops! This hoists and shadows the outer "name"
}
greet(); // "Hello, undefined" (not "Hello, Alice"!)
The engine sees:
var name = "Alice";
function greet() {
var name; // Hoisted! Shadows the outer "name"
console.log("Hello, " + name); // undefined (local "name" exists but has no value yet)
name = "Bob";
}
The local var name shadows the outer name, and because of hoisting, it exists (as undefined) before the assignment. With let, this would throw a ReferenceError in the TDZ, making the problem immediately obvious.
Function Declarations vs. var Hoisting
Function declarations are hoisted completely (both the name and the body). var hoists only the name:
sayHi(); // "Hi!" (function declarations are fully hoisted)
console.log(x); // undefined (var is hoisted but not initialized)
function sayHi() {
console.log("Hi!");
}
var x = 10;
But if you assign a function to a var, only the variable name is hoisted, not the function:
// sayBye(); // TypeError: sayBye is not a function
var sayBye = function() {
console.log("Bye!");
};
sayBye(); // "Bye!" (works after the assignment)
var Can Be Re-Declared
With let and const, declaring the same variable name twice in the same scope is a SyntaxError. With var, you can re-declare the same variable as many times as you want. The second (and subsequent) declarations are simply ignored by the engine:
var user = "Alice";
var user = "Bob"; // No error! Silently replaces.
var user = "Charlie"; // Still no error.
console.log(user); // "Charlie"
With let:
let user = "Alice";
// let user = "Bob"; // SyntaxError: Identifier 'user' has already been declared
Why Re-Declaration Is Dangerous
Re-declaration makes it easy to accidentally overwrite variables, especially in long functions or when copy-pasting code:
function processOrder(items) {
var total = 0;
for (var i = 0; i < items.length; i++) {
var price = items[i].price;
var quantity = items[i].quantity;
total += price * quantity;
}
// 50 lines of other code later...
// Someone adds this, not realizing "total" already exists:
var total = calculateShipping(); // Silently overwrites the previous total!
return total; // Returns only shipping cost, not items + shipping
}
With let, the second declaration would immediately throw a SyntaxError, catching the mistake at parse time.
Re-Declaration with Hoisting Creates Confusion
var count = 10;
if (true) {
var count = 20; // Same variable! Not a new block-scoped variable.
}
console.log(count); // 20 (the "inner" var modified the "outer" count)
With let:
let count = 10;
if (true) {
let count = 20; // Different variable! Block-scoped.
}
console.log(count); // 10 (the outer count is unchanged)
IIFE: The Legacy Pattern for Creating Scope
Since var has no block scope, developers before ES2015 needed a workaround to create isolated scopes. The solution was the Immediately Invoked Function Expression, or IIFE (pronounced "iffy").
What Is an IIFE?
An IIFE is a function that is defined and executed immediately. Since var is function-scoped, wrapping code in a function creates a private scope:
(function() {
var privateVar = "You can't see me from outside";
console.log(privateVar); // "You can't see me from outside"
})();
// console.log(privateVar); // ReferenceError: privateVar is not defined
The parentheses around the function are necessary. Without them, JavaScript would interpret the function keyword as a function declaration, which requires a name and cannot be immediately invoked:
// WRONG: SyntaxError
// function() { console.log("nope"); }();
// CORRECT: Wrapping in parentheses makes it an expression
(function() { console.log("works!"); })();
IIFE Syntax Variations
There are several ways to write an IIFE. They all work the same way:
// Style 1: Wrapping the function (most common)
(function() {
console.log("Style 1");
})();
// Style 2: Wrapping the entire call
(function() {
console.log("Style 2");
}());
// Style 3: Using unary operators (less common)
!function() {
console.log("Style 3 with !");
}();
+function() {
console.log("Style 3 with +");
}();
void function() {
console.log("Style 3 with void");
}();
// Style 4: Arrow function IIFE
(() => {
console.log("Arrow IIFE");
})();
IIFEs with Parameters
You can pass arguments to an IIFE:
(function(name, greeting) {
console.log(`${greeting}, ${name}!`);
})("Alice", "Hello");
// "Hello, Alice!"
IIFE for the Module Pattern (Pre-ES2015)
Before JavaScript had a module system, IIFEs were the standard way to create modules with private state:
var Calculator = (function() {
// Private variables (not accessible from outside)
var history = [];
// Private function
function addToHistory(operation, result) {
history.push({ operation, result, timestamp: Date.now() });
}
// Public API (returned as an object)
return {
add: function(a, b) {
var result = a + b;
addToHistory(`${a} + ${b}`, result);
return result;
},
subtract: function(a, b) {
var result = a - b;
addToHistory(`${a} - ${b}`, result);
return result;
},
getHistory: function() {
return history.slice(); // Return a copy, not the original
}
};
})();
console.log(Calculator.add(5, 3)); // 8
console.log(Calculator.subtract(10, 4)); // 6
console.log(Calculator.getHistory());
// [
// { operation: '5 + 3', result: 8, timestamp: ... },
// { operation: '10 - 4', result: 6, timestamp: ... }
// ]
// console.log(Calculator.history); // undefined (it's private)
// console.log(Calculator.addToHistory); // undefined (it's private)
This is called the Revealing Module Pattern. The IIFE creates a closure, and the returned object exposes only the public API. The internal variables and functions are truly private.
IIFE to Fix the Loop Closure Problem
As covered in the closures guide, var in loops causes a notorious bug with callbacks. IIFEs were the pre-let solution:
// BROKEN: All callbacks share the same "i"
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i); // Always 5
}, 100);
}
// Output: 5, 5, 5, 5, 5
// FIXED with IIFE: Each iteration gets its own scope
for (var i = 0; i < 5; i++) {
(function(capturedI) {
setTimeout(function() {
console.log(capturedI);
}, 100);
})(i);
}
// Output: 0, 1, 2, 3, 4
The IIFE creates a new function scope on each iteration, and capturedI is a new variable each time, capturing the current value of i.
IIFEs to Prevent Global Pollution
Libraries like jQuery used IIFEs to avoid polluting the global namespace:
// Old-school library pattern
(function(global) {
// All library code goes here
var internalState = {};
function internalHelper() {
// ...
}
// Expose only what's needed
global.MyLibrary = {
publicMethod: function() {
internalHelper();
return internalState;
}
};
})(window);
// Only "MyLibrary" is added to the global scope
// internalState, internalHelper are hidden
IIFEs are rarely needed in modern JavaScript. With let, const, and ES modules (import/export), you have proper block scoping and module scoping built into the language. However, understanding IIFEs is important for reading legacy code and understanding how the JavaScript ecosystem evolved.
Why var Still Exists in Codebases and How to Migrate
Where You Will Encounter var
Even though let and const were introduced in 2015, var is still everywhere:
- Legacy applications built before ES2015 or during the transition period
- Older tutorials and books (many popular resources still use
var) - Minified/bundled code where tools sometimes use
varfor output - Interview questions that test your understanding of hoisting and scope
- Stack Overflow answers written before
let/constbecame standard - Scripts targeting very old browsers without transpilation
The Migration Strategy
Converting var to let/const is usually straightforward, but it requires attention. Here is a step-by-step approach:
Step 1: Identify variables that are never reassigned. Change those to const.
// Before
var API_URL = "https://api.example.com";
var MAX_RETRIES = 3;
// After
const API_URL = "https://api.example.com";
const MAX_RETRIES = 3;
Step 2: Change remaining var to let.
// Before
var count = 0;
var name = getUserName();
// After
let count = 0;
let name = getUserName();
Step 3: Check for scope-dependent behavior.
This is where you need to be careful. If the code relies on var leaking out of blocks, changing to let will break it:
// This code RELIES on var's block-leak behavior
function findItem(items, target) {
for (var i = 0; i < items.length; i++) {
if (items[i] === target) {
break;
}
}
return i < items.length ? i : -1; // Uses "i" AFTER the loop
}
If you change var i to let i, the return statement cannot access i because it is outside the for block. You need to restructure:
// Correct migration
function findItem(items, target) {
let foundIndex = -1;
for (let i = 0; i < items.length; i++) {
if (items[i] === target) {
foundIndex = i;
break;
}
}
return foundIndex;
}
// Or even better, use the built-in method:
function findItem(items, target) {
return items.indexOf(target);
}
Step 4: Check for re-declarations.
If the code re-declares the same variable name with var, changing to let will cause a SyntaxError:
// Works with var
var result = computeA();
// ... some code ...
var result = computeB(); // Re-declaration
// Fails with let
let result = computeA();
// let result = computeB(); // SyntaxError!
// Fix: just reassign without re-declaring
let result = computeA();
// ... some code ...
result = computeB(); // Simple reassignment
Step 5: Check for hoisting reliance.
Some (usually poorly written) code relies on var being accessible before its declaration:
// Relies on var hoisting
function init() {
setupUI();
// ... many lines later ...
var config = loadConfig(); // Used above via hoisting? Probably not intentional.
}
With let, you must declare the variable before using it. This is actually a benefit because it forces clearer code organization.
Automated Tools for Migration
Several tools can help automate the var to let/const conversion:
- ESLint with the
no-varrule will flag everyvarusage - ESLint with the
prefer-construle will suggestconstwhere possible - jscodeshift can automate the transformation across an entire codebase
- IDE refactoring tools in VS Code and WebStorm can convert
vartolet/const
ESLint configuration to enforce modern variable declarations:
{
"rules": {
"no-var": "error",
"prefer-const": "error"
}
}
Should You Ever Use var in New Code?
No. There is no situation in modern JavaScript where var is the better choice over let or const. The only exception is if you are writing code that must run in very old browsers (like Internet Explorer 11) without any transpilation. In practice, even that scenario is handled by Babel, which transpiles let/const back to var automatically.
Complete Comparison Table
| Behavior | var | let | const |
|---|---|---|---|
| Scope | Function | Block | Block |
| Hoisting | Yes, initialized to undefined | Yes, but in TDZ | Yes, but in TDZ |
| Re-declaration | Allowed | SyntaxError | SyntaxError |
| Re-assignment | Allowed | Allowed | Not allowed |
| Global object property | Yes (at top level) | No | No |
| Use in modern code | Avoid | For values that change | For values that do not change |
Summary
| Concept | Key Takeaway |
|---|---|
| Function scope | var is scoped to the nearest function, ignoring all block boundaries |
| Hoisting | var declarations are moved to the top of their scope; the value remains undefined until the assignment line |
| Re-declaration | var allows declaring the same variable name multiple times without error |
| IIFE | The legacy workaround for creating block-like scope before let/const existed |
| Module pattern | IIFEs enabled private variables and public APIs before ES modules |
| Migration | Replace var with const (if never reassigned) or let (if reassigned), then check for scope-dependent code |
| Modern recommendation | Never use var in new code; use const by default, let when reassignment is needed |
Understanding var is not about using it. It is about recognizing its behavior when you encounter it in existing code, understanding why certain legacy patterns exist, and appreciating how much cleaner modern JavaScript is with let and const. Every time you see an IIFE or a confusing hoisting bug in legacy code, you will now know exactly what is happening and how to fix it.