Open any JavaScript project written in the last few years and you will see all three keywords side by side: var, let and const. If you are learning, it is natural to ask why a language would need three ways to do the same job. The honest answer is that they do not do the same job, and that the oldest of them was a design mistake the language has spent a decade quietly working around. This guide explains what each one does, where the real differences bite, and which one to reach for every time you write a line of code.
Start with the simple idea: a variable is a label
Before comparing keywords, let us agree on what a variable is. A variable is a name attached to a value so that you can use that value later. Imagine a jar in your kitchen with a sticky label that says “sugar”. The label lets you find what is inside without opening every jar. In code, price is the label and 250 is what is in the jar.
When you declare a variable, you are doing two things: creating the label, and deciding the rules for that label. Can you peel it off and stick it on a different jar later? Who in the house is allowed to see it? Can you use the label before you have written it? Those three questions, reassignment, scope and timing, are exactly where var, let and const differ.
The short version. Use const by default. Use let when the value must change. Avoid var in new code. The longer version, with the reasons, is below, and the reasons are what stick.
Why does JavaScript have three?
JavaScript was created in 1995 and for its first twenty years it had exactly one way to declare a variable: var. It worked, but its rules were different from nearly every other popular language, and those differences caused bugs that confused beginners and experts alike.
In 2015, the language standard known as ES2015 (many people call it ES6) added two new keywords, let and const, with rules that behave the way programmers coming from other languages expect. The old keyword could not be changed or removed, because millions of existing websites depend on it. So JavaScript ended up with all three. That is the whole story: two modern tools sitting next to an old one that has to stay for compatibility.
The first big difference: scope
Scope answers the question: where in my code can I use this variable? A useful way to picture it is a house. A function scope is the whole house: anyone inside, in any room, can use what is stored in the hallway. A block scope is a single room: what you put in the bedroom stays in the bedroom. A block in JavaScript is any pair of curly braces, such as the body of an if statement or a for loop.
varis function-scoped. It ignores the curly braces of an if or a loop and is visible everywhere in the surrounding function.letandconstare block-scoped. They exist only inside the braces where you declared them.
I do not want you to take that on trust, so here is a short test. It declares one variable of each kind inside an if block and then tries to use each one outside it.
// 1. Scope: does the variable survive outside the block?
if (true) {
var oldWay = "I am var";
let newWay = "I am let";
const fixed = "I am const";
}
console.log("var outside the block :", oldWay);
try { console.log(newWay); } catch (e) { console.log("let outside the block :", e.name + ": " + e.message); }
try { console.log(fixed); } catch (e) { console.log("const outside the block:", e.name + ": " + e.message); }
// 2. A loop counter
for (var i = 0; i < 3; i++) {}
console.log("var i after the loop :", i);
for (let j = 0; j < 3; j++) {}
console.log("typeof j after loop :", typeof j);
// 3. Redeclaring
var price = 100;
var price = 250; // allowed, silently
console.log("var redeclared :", price);
Output (run with Node 22):
var outside the block : I am var
let outside the block : ReferenceError: newWay is not defined
const outside the block: ReferenceError: fixed is not defined
var i after the loop : 3
typeof j after loop : undefined
var redeclared : 250
The var value leaked out of the block and printed normally. The let and const values did not exist outside the block, so JavaScript threw a ReferenceError. The loop counter shows the same behaviour: after a for (var i...) loop, i is still alive and holds 3. After a for (let j...) loop, j is gone.
Leaking sounds harmless, but think about a long function with several loops that all use var i. They share one variable. A value left behind by the first loop can quietly change the behaviour of the third. Block scope is a safety feature: it keeps each piece of code from trampling the variables of its neighbours.
The second difference: redeclaring
With var, you can declare the same name twice in the same scope and JavaScript says nothing. The last line of the scope test shows it: var price = 100; followed by var price = 250; is allowed, and the value simply becomes 250.
That is dangerous in a big file. Suppose you are 400 lines into a function and you declare var total, forgetting that you already used total near the top for something else. You have just overwritten the earlier value with no warning. With let and const, declaring the same name twice in the same scope is a syntax error, and your editor will usually underline it before you even run the code. A loud mistake is far cheaper than a silent one.
The third difference: hoisting and the “dead zone”
This is the topic that scares people, but the idea is simple. Before JavaScript runs your code, it reads through it once and notes every declaration. This is called hoisting, as if the declarations were lifted to the top of their scope. What happens to the variable next depends on the keyword.
varis hoisted and given the starting value undefined. You can read it before its line, and you just get undefined.letandconstare hoisted too, but they stay uninitialised until execution reaches their line. Touching them earlier throws an error. The stretch between the top of the scope and the declaration line is called the temporal dead zone.
// Hoisting: using a variable before the line that declares it
console.log("var before its line :", early);
var early = 5;
try {
console.log(late);
} catch (e) {
console.log("let before its line :", e.name + ": " + e.message);
}
let late = 5;
try {
console.log(never);
} catch (e) {
console.log("const before its line :", e.name + ": " + e.message);
}
const never = 5;
// A function declaration is hoisted completely
console.log("function hoisted :", greet());
function greet() { return "hello"; }
Output:
var before its line : undefined
let before its line : ReferenceError: Cannot access 'late' before initialization
const before its line : ReferenceError: Cannot access 'never' before initialization
function hoisted : hello
The var variable quietly printed undefined, which is the worst kind of result: no crash, but a wrong value flowing into the rest of your program. The let and const versions failed immediately with a clear message: “Cannot access ‘late’ before initialization”. The error names the variable and tells you what went wrong, so you fix it in seconds.
The last line shows that function declarations are the exception. A function declared with the function keyword is hoisted completely, body and all, so you can call it above where it is written. A function stored in a variable, such as var greet = function() {...}, follows the variable rules instead: the name exists but holds undefined, and calling it early gives a TypeError.
The fourth difference: reassigning
Here let and const part company. A let variable can be given a new value as often as you like. A const variable cannot be pointed at something else once it has been set. It also must be given a value at the moment it is declared.
Use let for things that genuinely change: a running total, a counter, a flag that flips when something happens. Use const for everything else. In real programs, the large majority of variables never need to change after they are first set, so const ends up being the most common keyword in well-written code.
The part everyone gets wrong: const is not “frozen”
New developers often assume const means the value can never change. It does not. It means the label cannot be moved to a different jar. If the jar holds an object or an array, you can still change what is inside it. Watch:
"use strict";
const rate = 0.18;
try { rate = 0.2; } catch (e) { console.log("reassign const :", e.name + ": " + e.message); }
const cart = ["rice", "dal"];
cart.push("tea"); // allowed: the array changes, the label does not
console.log("push into const array :", cart);
try { cart = ["new"]; } catch (e) { console.log("point const elsewhere :", e.name + ": " + e.message); }
const user = { name: "Asha", plan: "free" };
user.plan = "pro"; // allowed
console.log("edit const object :", user);
const locked = Object.freeze({ plan: "free" });
try { locked.plan = "pro"; } catch (e) { console.log("frozen object edit :", e.name + ": " + e.message); }
console.log("frozen stays :", locked);
let count = 0;
count = count + 1;
console.log("let can be reassigned:", count);
Output:
reassign const : TypeError: Assignment to constant variable.
push into const array : [ 'rice', 'dal', 'tea' ]
point const elsewhere : TypeError: Assignment to constant variable.
edit const object : { name: 'Asha', plan: 'pro' }
frozen object edit : TypeError: Cannot assign to read only property 'plan' of object '#<Object>'
frozen stays : { plan: 'free' }
let can be reassigned: 1
Reassigning the constant failed, as expected. But pushing a new item into a const array worked, and so did changing a property of a const object. The label still points at the same array and the same object; only their contents changed. If you truly need an object that cannot be edited, Object.freeze() locks it, as the last lines show. The freeze is shallow, meaning it protects the top level only, so nested objects still need their own freezing if you want them locked.
The sneaky one: var and the global object
There is one more difference that matters in browsers. A var declared at the top level of a normal script becomes a property of the global window object. That means it can collide with other scripts on the same page, including ad scripts, analytics and plugins. A let or const at the top level does not attach to window. I tested it in a real browser:
// Run as a normal <script> in a browser page
var a = 1;
let b = 2;
const c = 3;
console.log(window.a, window.b, window.c);
Output:
1 undefined undefined
Only the var landed on window. On a WordPress site with a dozen plugins each adding scripts, that is not a small matter. Two plugins that both declare var data at the top level can overwrite each other and produce bugs that appear only on certain pages.
The bug that made everyone switch: loops and callbacks
If you only remember one example from this article, make it this one. You want three delayed messages, each reporting its own loop number. Here is the code with var and then with let:
// The classic bug: three delayed callbacks
console.log("with var:");
for (var a = 0; a < 3; a++) {
setTimeout(() => console.log(" var sees", a), 0);
}
setTimeout(() => {
console.log("with let:");
for (let b = 0; b < 3; b++) {
setTimeout(() => console.log(" let sees", b), 0);
}
}, 10);
Output:
with var:
var sees 3
var sees 3
var sees 3
with let:
let sees 0
let sees 1
let sees 2
With var every callback printed 3. With let they printed 0, 1 and 2, as you probably hoped. Why? A var loop counter is one single variable shared by the whole function. By the time the delayed callbacks run, the loop has already finished and the variable holds its final value of 3, so every callback sees 3. A let loop counter gets a fresh copy for each pass through the loop, and each callback remembers its own copy.
This is not an obscure puzzle. It shows up in real pages: a row of buttons where each one should report its own number, a photo gallery where each thumbnail should open its own image, a set of tabs. Before let existed, the standard fix was to wrap the body of the loop in an extra function, a pattern that confused many newcomers. Today the fix is to change one word.
All the differences in one table
| Property | var | let | const |
|---|---|---|---|
| Scope | Function | Block | Block |
| Reassign | Yes | Yes | No |
| Redeclare in same scope | Allowed | Error | Error |
| Use before declaration | undefined | ReferenceError | ReferenceError |
| Needs a value when declared | No | No | Yes |
| Added to window at top level | Yes | No | No |
| Introduced | 1995 | ES2015 | ES2015 |
| Use it for | Legacy code only | Values that change | Everything else |
Real-life examples: which keyword fits?
Rules are easier to remember when they are tied to everyday situations. Click through the five tabs below. Each one is a small, real scenario and the keyword that suits it.
Cart total
A shopping cart adds up prices as the customer picks items. How should the running total be declared?
The right keyword. The total starts at zero and is replaced every time an item is added, so it must be reassignable. That is exactly the job of let total = 0;. If you wrote const total = 0;, the first addition would throw “Assignment to constant variable”, which at least tells you straight away that this one needs let.
Tax rate
A billing page applies a fixed tax rate of 18 percent to every invoice. Which keyword fits?
The right keyword. The rate is set once and never reassigned, so const is right. It also documents your intent: anyone reading the code knows this number is not supposed to move. When the law changes, you edit that one line instead of hunting for a value that something might have altered.
Login attempts
A login form allows three wrong passwords before locking. How do you declare the counter and the limit?
The right keyword. The counter changes each time, so it is a let. The limit of three never changes, so it is a const and is often written in capitals. Keeping them separate also makes the rule easy to adjust later: change MAX in one place and the whole form follows.
Gallery buttons
A photo gallery builds five thumbnail buttons in a loop. Each one should open its own photo number when clicked. What goes wrong with var?
The right keyword. With for (var i...) all five click handlers share one variable, and by the time anyone clicks, the loop has finished and every button reports the last number. With for (let i...) each pass gets its own copy, so each button remembers its own number. This is the same behaviour as the 3, 3, 3 test earlier.
Old plugin script
You inherit a script from 2012 that uses var everywhere and runs on a busy page. Should you rewrite every var?
The right keyword. Not in one go. Old code may depend on function scope or hoisting, and a blanket find-and-replace can break it. Read each var as function-scoped, convert variables when you are already editing nearby lines, and test after each change. Also watch top-level vars: they attach to window and can collide with other scripts on the page.
A simple decision guide
When I review code, I ask the same short sequence of questions. It takes a few seconds, and after a week it becomes automatic.
Habits that save you from bugs
- Default to const. If the linter or your editor complains that you tried to reassign, you have just learned something useful: this variable changes, so switch that one to
let. Starting with const makes the changing variables stand out. - Declare as late as you can. Put the variable close to where it is first used and in the narrowest block possible. Short lifetimes mean fewer places for things to go wrong.
- One declaration per line. Writing
let a = 1, b = 2, c = 3;on one line is legal, but separate lines are easier to read and easier to change. - Use a linter. ESLint has the rules
no-varandprefer-const, which flag exactly the habits in this article automatically. - Pick descriptive names.
const taxRatetells you a lot more thanconst t. Constants that are truly fixed, like a maximum limit, are often written in capitals, such asMAX_ITEMS. - Do not choose for speed. For ordinary code, the three keywords perform about the same. Pick for clarity, not performance.
Common mistakes and how to avoid them
- Thinking const means immutable. It locks the label, not the contents. Objects and arrays stored in a const can still change.
- Using var in a loop with callbacks. Every callback ends up seeing the final value. Use
let. - Declaring let twice in one scope. That is a syntax error. Check whether you really want a second variable or just a new value.
- Using a variable before its declaration. With let and const this throws an error. Move the declaration up, or restructure the code.
- Declaring the same let inside two switch cases. All the cases of a switch share one block, so two
let xlines clash. Wrap each case body in its own curly braces. - Declaring a const without a value.
const total;is a syntax error. A const must be set when it is created. - Rewriting old var code blindly. Changing every var to let can break code that relied on function scope or hoisting. Convert gradually, and test as you go.
- Forgetting that top-level var joins window. In a browser script, that can clash with other scripts. Use let, const or a module.
What about reading old code full of var?
You will meet it. Older tutorials, Stack Overflow answers from before 2015 and many WordPress plugins still use var everywhere. That is not a reason to panic, and it is not a reason to copy the habit. Read the code knowing that every var is function-scoped and hoisted, mentally move its declaration to the top of the function, and you will understand how it behaves.
When you write new code, use the modern keywords. When you edit old code, resist the urge to convert everything in one sitting. Change a variable when you are already touching the lines around it, run your tests, and move on. This is how experienced teams modernise large codebases without breaking them.
A mental model that sticks
Here is how I explain it to new developers. Imagine a whiteboard in a meeting room. A let variable is something written in erasable marker: you can wipe it and write a new value. A const variable is written in permanent marker: you cannot rewrite it, though if the thing you wrote is the name of a notebook, the notebook’s pages can still change. A var variable is a note that gets written on the whiteboard in the corridor as well: everyone in the building can read it, and anyone can overwrite it by accident.
Practice problems with answers
Try each one before you tap to check.
1. Which keyword suits a tax rate that never changes?
const. It is set once and never reassigned.
2. What does if (true) { var a = 1; } console.log(a); print?
1. A var ignores the block and is visible in the whole function.
3. What happens if you replace var with let in the previous question?
A ReferenceError. The let variable exists only inside the braces, so it is not defined outside.
4. Can you call push on an array stored in a const?
Yes. The label cannot be moved, but the array contents can change.
5. What does console.log(x); var x = 5; print?
undefined. The var is hoisted with the value undefined, and 5 is assigned later.
6. A loop of setTimeout callbacks prints 3, 3, 3. What is the one-word fix?
Change var to let in the loop header, so each pass gets its own copy.
7. Why does const total; fail?
A const must be given a value when it is declared. It produces a SyntaxError about a missing initializer.
Test yourself: five quick questions
Tap a question to reveal the answer. The correct option is marked.
Which keyword is function-scoped instead of block-scoped?
- const
- let
- none of them
- var
var ignores curly braces and is visible throughout its function. let and const are block-scoped.
You declare const list = [1, 2];. What does list.push(3) do?
- Throws a TypeError
- Works, and the array becomes [1, 2, 3]
- Creates a new constant
- Does nothing
const stops reassignment of the label, not changes to the array it points to.
What do three setTimeout callbacks in a var loop print?
- 3, 3, 3
- 0, 1, 2
- undefined three times
- An error
They all share one var, which holds 3 by the time they run.
What is the temporal dead zone?
- A bug in old browsers
- The time before a page loads
- The span between the start of a scope and the line where a let or const is declared
- A way to freeze objects
Touching the variable inside that span throws a ReferenceError.
Which keyword should you use by default in modern code?
- var
- const
- let
- It does not matter
Use const unless the value truly must be reassigned, then switch that one variable to let.
Frequently asked questions
What is the main difference between var, let and const?
var is function-scoped and can be redeclared. let and const are block-scoped and cannot be declared twice in the same scope. let allows reassignment, while const does not.
Should I still use var?
In new code, no. Use const or let. You will still read var in older code, and it remains valid because websites depend on it, but it has no advantage over the modern keywords.
Does const make a value immutable?
No. It prevents the variable from being reassigned. The contents of an object or array stored in a const can still change, unless you freeze it with Object.freeze().
What is hoisting?
JavaScript registers declarations before running the code. A var is hoisted with the value undefined, while let and const are hoisted but stay unusable until their declaration line runs.
Is let slower than var?
For ordinary code, there is no practical difference. Choose the keyword for clarity and safety, not for speed.
Can I declare a const without a value?
No. A const must be initialised when it is declared, otherwise JavaScript raises a SyntaxError.
Key takeaways
- var is function-scoped, hoisted as undefined and can be redeclared. let and const are block-scoped and throw an error if used early or declared twice.
- The loop test printed 3, 3, 3 with var and 0, 1, 2 with let, because let gives each pass of the loop its own copy.
- const stops you from reassigning the variable, but not from changing the contents of an object or array it points to. Object.freeze() locks the top level.
- A top-level var in a browser script becomes a property of window. My browser test showed 1 for var and undefined for let and const.
- Default to const, switch to let only when the value must change, and treat var as a sign you are reading legacy code.
