The old "var"
Back when we first looked at variables, three keywords declared them:
letconstvar
var looks almost identical to let. Swap one for the other in casual code and it usually behaves the same:
var greeting = "Hey";
alert(greeting); // Hey
Under the hood, though, var follows rules from an older era of the language. You rarely reach for it now, but it still lingers in scripts written years ago.
If you never expect to touch such code, you could skip this chapter or come back to it later. But the moment you migrate something from var to let, these differences matter — miss one and you get errors that look like they came out of nowhere.
Here’s the map of what makes var its own animal:
“var” has no block scope
A variable declared with var is scoped to the nearest enclosing function, or to the global scope if no function wraps it. Code blocks — the { } of an if, a for, a bare block — are invisible to it.
Watch a variable survive the if block it was born in:
if (true) {
var flag = true; // used "var" instead of "let"
}
alert(flag); // true, the variable outlives the if
Because var steps right over the braces, flag ends up global.
Swap in let, and the variable stays trapped inside the if where it belongs:
if (true) {
let flag = true; // used "let"
}
alert(flag); // ReferenceError: flag is not defined
Loops behave the same way. A var counter or a var declared in the loop body can’t be loop-local:
for (var i = 0; i < 10; i++) {
var step = 1;
// ...
}
alert(i); // 10 — "i" survives the loop, it's global
alert(step); // 1 — "step" survives the loop, it's global
Put that same block inside a function and the reach changes: instead of going global, var climbs only to the function’s top level.
function greet() {
if (true) {
var note = "Ready";
}
alert(note); // works — note is function-scoped
}
greet();
alert(note); // ReferenceError: note is not defined
So var punches through if, for, and any other block, but a function boundary stops it cold. The reason is historical: early JavaScript blocks had no lexical environment of their own, and var is a leftover from that design.
“var” tolerates redeclarations
Declare the same name twice with let in one scope and the engine refuses:
let user;
let user; // SyntaxError: 'user' has already been declared
var shrugs it off. Repeat a var declaration as many times as you like — a second var on an existing name is treated as a no-op, and if it carries an assignment, that assignment simply runs:
var user = "Raj";
var user = "Maya"; // the second "var" adds nothing new,
// but the assignment still happens — no error either way
alert(user); // Maya
This tolerance sounds convenient and turns out to be a trap. In a long function it’s easy to var the same name twice by accident and quietly clobber an earlier value with no warning from the engine.
“var” variables can be declared below their use
var declarations are processed the instant a function starts — or the instant the script starts, for globals. Whatever the position of the var line in the source, the declaration is treated as if it sat at the top of the function (as long as it isn’t tucked inside a nested function).
So this code:
function greet() {
note = "Ready";
alert(note);
var note;
}
greet();
…is equivalent to writing the declaration up top:
function greet() {
var note;
note = "Ready";
alert(note);
}
greet();
…and even to this, since blocks don’t matter to var:
function greet() {
note = "Ready"; // (*)
if (false) {
var note;
}
alert(note);
}
greet();
This raising-to-the-top behavior has a name: hoisting. Every var is “hoisted” to the top of its function.
In that last example the if (false) branch never runs, yet it makes no difference. The var inside it is registered at function start, so by the time execution hits (*) the variable already exists.
Declarations are hoisted. Assignments are not.
That distinction is the whole story, and this example makes it plain:
function greet() {
alert(note);
var note = "Ready";
}
greet();
The line var note = "Ready" packs two separate actions:
- The declaration
var note - The assignment
note = "Ready"
Only the declaration moves to the top. The assignment stays exactly where you wrote it. So the engine runs the code as if it were:
function greet() {
var note; // declaration hoisted to the start...
alert(note); // undefined — declared but not yet assigned
note = "Ready"; // ...assignment runs only when execution reaches it
}
greet();
Because the declaration is always processed at function start, referencing the name early never throws — the variable exists. It just holds undefined until its assignment line actually executes.
Both earlier examples ran alert without error for exactly this reason: note was already declared, only its value hadn’t arrived yet, so you saw undefined.
IIFE
Because var had no block scope and there was nothing else, developers of the era invented a way to carve out a private scope by hand: the immediately-invoked function expression, or IIFE.
You won’t write these today, but you’ll spot them all over older libraries, so it helps to recognize the shape:
(function() {
var banner = "Ready";
alert(banner); // Ready
})();
A function expression is created and called on the spot. The code runs immediately, and any var inside stays sealed off in that function’s scope — exactly the privacy that blocks couldn’t provide.
Notice the parentheses wrapping the function: (function {...}). They’re load-bearing. When the engine meets the keyword function at the start of a statement, it assumes a function declaration is coming. A function declaration needs a name, so leaving it out is a syntax error:
// tries to declare a function and call it right away
function() { // <-- SyntaxError: Function statements require a function name
var banner = "Ready";
alert(banner); // Ready
}();
Adding a name doesn’t rescue it either. JavaScript won’t let you call a function declaration the moment it’s defined, so the trailing () is a syntax error:
// syntax error because of the parentheses below
function go() {
}(); // <-- can't immediately call a Function Declaration
Wrapping the function in parentheses fixes both problems at once. Inside parentheses the engine is parsing an expression, so function now begins a function expression — which needs no name and can be invoked right away.
Parentheses aren’t the only signal. Any operator that forces the engine into expression-parsing mode works. Here are several idioms you might run across:
// Ways to create an IIFE
(function() {
alert("Parentheses around the function");
})();
(function() {
alert("Parentheses around the whole thing");
}());
!function() {
alert("Bitwise NOT operator starts the expression");
}();
+function() {
alert("Unary plus starts the expression");
}();
Each one declares a function expression and fires it immediately. And once more, for the record: there’s no reason to write code like this in a modern codebase — a plain block with let gives you private scope for free.
Summary
Two differences dominate when comparing var to let and const:
- No block scope. A
varis visible throughout the current function, or globally when declared outside any function. Blocks likeifandfordon’t contain it. - Processed at function start.
vardeclarations are hoisted to the top of the function (or to the top of the script for globals), so the name exists before its line runs — holdingundefineduntil the assignment executes.
There’s also one small extra difference tied to the global object, which the next chapter covers.
Together these make var the weaker choice almost every time. Block scoping is a genuinely useful guardrail, and it’s the reason let (with const) landed in the standard and became the default way to declare variables.