Loading
All articles
Programming 16 min read

JavaScript: Stop Using var. A Visual Guide to const and let

Why modern JavaScript uses const and let instead of var: reassignment, block vs. function scope, the loop-closure bug, hoisting and the temporal dead zone, redeclaration, real Node.js runs, a speed test, and exercises with answers.

nbkhope
On this page

For its first twenty years, JavaScript had exactly one way to declare a variable: var. Then ES6 (also called ES2015) added two more, const and let, and today most style guides boil the advice down to three words: stop using var.

That isn't just fashion. var has quirks that cause real bugs: variables that leak out of blocks, loops whose callbacks all see the same number, and variables you can use before they're declared. In this guide you'll see each quirk run for real, learn the simple rule for choosing between const and let, and practice spotting and fixing var bugs. Exercises with answers wrap it up.

All the outputs below come from real runs with Node.js 26. They're the same in every modern browser.

The rule in one picture

Which keyword should I use? Start with const. Will this variable ever be given a new value with =? If no, keep const: most variables, including arrays and objects you change with push or by setting properties. If yes, use let: counters, running totals, loop indexes, values that change later. And var? Never in new code: its scope leaks out of blocks and its loops share one variable. Start with const; switch to let only when you have to reassign
1// The old way
2var name = "Angela";
3
4// The new way
5const name = "Angela";
  1. Start with const. Most variables are given a value once and never reassigned.
  2. Switch to let only when you need to reassign the variable with = later.
  3. Don't use var in new code.

Let's see why, one behavior at a time.

const: assigned once, for good

A const variable gets its value when it's declared and can never be reassigned:

1const greeting = "Hello";
2console.log(greeting);
3
4try {
5  greeting = "Goodbye";
6} catch (error) {
7  console.log(`${error.name}: ${error.message}`);
8}
Hello
TypeError: Assignment to constant variable.

(We catch the error so the program can print it and keep going.) Because it can never be assigned later, a const must get a value right away. This program doesn't even start:

const answer;
SyntaxError: Missing initializer in const declaration

That's the point of const: when you read code and see const total = …, you know total holds that value everywhere below. No hunting for a line that changes it.

const locks the label, not the box

Here's the part that surprises people. You can still change what's inside a const array or object:

1const pets = [];
2pets.push("cat");
3pets.push("dog");
4console.log(pets);
5
6const settings = { theme: "light" };
7settings.theme = "dark";
8settings.fontSize = 16;
9console.log(settings);
10
11try {
12  pets = ["hamster"];
13} catch (error) {
14  console.log(`${error.name}: ${error.message}`);
15}
[ 'cat', 'dog' ]
{ theme: 'dark', fontSize: 16 }
TypeError: Assignment to constant variable.
const locks the label, not the box. The variable pets is a label permanently attached to one array. pets.push("cat") and pets.push("dog") are allowed: they change what is inside the same array, which now holds "cat" and "dog". pets = ["hamster"] is not allowed: it tries to move the label to a different array, giving TypeError: Assignment to constant variable. const means "this name always refers to this value", not "this value never changes". To freeze the contents too, use Object.freeze, which is shallow

A variable is a label stuck on a value. const glues the label to one array forever, but the array itself can still change. Only pets = …, which tries to move the label, is forbidden.

If you want the contents locked too, Object.freeze does that, but only one level deep:

1const place = Object.freeze({ city: "Lisbon", tags: ["sea"] });
2place.city = "Porto";
3place.tags.push("sun");
4console.log(place);
{ city: 'Lisbon', tags: [ 'sea', 'sun' ] }

The city didn't change (outside strict mode the assignment is silently ignored; inside it, it throws), but the array inside could still grow.

let: when the value has to change

Use let when the variable genuinely gets new values over time: counters, running totals, loop indexes, or a value decided by an if:

1let total = 0;
2for (const price of [3.5, 4.25, 2]) {
3  total += price;
4}
5console.log(total);
6
7let message = "Good morning";
8const hour = 15;
9if (hour >= 12) {
10  message = "Good afternoon";
11}
12console.log(message);
9.75
Good afternoon

Notice the loop uses const price: each trip around a for…of loop gets a brand-new price, so it's never reassigned. A classic counting loop is different, because index++ reassigns the variable:

1const letters = ["a", "b", "c"];
2for (let index = 0; index < letters.length; index++) {
3  console.log(index, letters[index]);
4}
0 a
1 b
2 c

Scope: where a variable exists

Scope is the region of code where a variable can be used. This is where var and let/const differ the most:

  • let and const are block-scoped: they exist only inside the nearest pair of curly braces { }, like an if body or a loop.
  • var is function-scoped: it ignores blocks and belongs to the whole function around it.
Block scope vs. function scope. A function contains an if block. let inner = 2 and const label = "x", declared inside the if block's curly braces, exist only inside that block: using them after the closing brace gives ReferenceError: inner is not defined. var leaky = 1, declared inside the same if block, belongs to the whole function, so it is still there after the block ends. let and const live in the nearest { }. var ignores { } and lives in the whole function (or the whole script, if not in a function)
1function checkScopes() {
2  if (true) {
3    var leaky = "var";
4    let contained = "let";
5  }
6  console.log(leaky);
7  try {
8    console.log(contained);
9  } catch (error) {
10    console.log(`${error.name}: ${error.message}`);
11  }
12}
13
14checkScopes();
var
ReferenceError: contained is not defined

The same thing happens with loop variables. A var index outlives its loop:

1for (var oldIndex = 0; oldIndex < 3; oldIndex++) {
2  console.log(oldIndex);
3}
4console.log("after the var loop:", oldIndex);
5
6for (let newIndex = 0; newIndex < 3; newIndex++) {
7  console.log(newIndex);
8}
9try {
10  console.log("after the let loop:", newIndex);
11} catch (error) {
12  console.log(`${error.name}: ${error.message}`);
13}
10
21
32
4after the var loop: 3
50
61
72
8ReferenceError: newIndex is not defined

A leaked variable might seem harmless, but it means a loop counter or a temporary value can collide with another variable of the same name elsewhere in the function. Block scope keeps each variable where it belongs.

The classic loop bug

This is the bug that made many people switch for good. Each loop iteration schedules a function to run a moment later:

1for (var index = 0; index < 3; index++) {
2  setTimeout(() => console.log("var:", index), 0);
3}
4
5for (let index = 0; index < 3; index++) {
6  setTimeout(() => console.log("let:", index), 0);
7}
1var: 3
2var: 3
3var: 3
4let: 0
5let: 1
6let: 2
Why the var loop prints 3, 3, 3. With var there is one shared index variable for the whole loop. The three functions all remember that same variable, and by the time they run, the loop has finished and index is 3, so they print 3, 3, 3. With let, each iteration gets its own fresh copy of index, holding 0, 1 and 2, and each function remembers its own copy, so they print 0, 1, 2. The callbacks run after the loop ends: setTimeout, click handlers and promises all work this way

The functions run after the loop has finished. With var there's a single index for the whole loop, and by then it's 3. With let, JavaScript creates a fresh index for every iteration, so each function remembers its own value. The same bug shows up with click handlers, timers and promises created in a loop, and let fixes all of them without any extra code.

Hoisting and the temporal dead zone

JavaScript sets up a function's variables before running its first line. That's called hoisting. What a variable holds before its declaration line is where the keywords differ:

1function beforeDeclaring() {
2  console.log("var before its line:", early);
3  var early = "ready";
4  console.log("var after its line:", early);
5
6  try {
7    console.log(late);
8  } catch (error) {
9    console.log(`${error.name}: ${error.message}`);
10  }
11  let late = "ready";
12}
13
14beforeDeclaring();
var before its line: undefined
var after its line: ready
ReferenceError: Cannot access 'late' before initialization
Hoisting and the temporal dead zone, line by line through a function. With var early: from the start of the function until the line var early = "ready", early exists and quietly holds undefined; after that line it holds "ready". With let late: from the start of the function until the line let late = "ready" is the temporal dead zone, where any use throws ReferenceError: Cannot access 'late' before initialization; after the line it holds "ready". var hides the mistake by handing you undefined; let reports it right away

A var exists from the top of its function and quietly holds undefined until its line runs, so a mistake gives you a wrong value instead of an error. A let or const also exists from the top of its block, but touching it before its line throws. That stretch is called the temporal dead zone. Loud errors are good: they point at the mistake instead of letting undefined travel through your program.

Declaring the same name twice

var lets you declare a variable again without a peep, which can silently overwrite one that's already in use:

var score = 10;
var score = 0;
console.log(score);
0

With let or const, the program refuses to run:

let score = 10;
let score = 0;
SyntaxError: Identifier 'score' has already been declared

And one more var quirk: in a regular browser script, a var declared at the top level becomes a property of the global window object, where any other script on the page can read or overwrite it. Top-level let and const don't.

Is any of them faster?

No. We timed the same summing loop written three ways, with 50 million iterations:

var, let and const run at the same speed. Time to run the same summing loop 50 million times, measured for this article with Node.js 26.3 on Windows: median of 7 runs, then the median of 3 program runs. var: 64.8 milliseconds. let: 62.7 milliseconds. const: 63.9 milliseconds. About 1.3 nanoseconds per iteration for all three. The differences are measurement noise. Choose by correctness and clarity, not speed

All three came in at about 1.3 nanoseconds per iteration, and the small differences moved around from run to run. JavaScript engines compile all three to the same fast code, so choose by clarity and correctness alone.

Side by side

varletconst
Can be reassignedYesYesNo
Must be given a value right awayNoNoYes
ScopeWhole functionNearest { } blockNearest { } block
Fresh copy per loop iterationNo (one shared variable)YesYes (in for…of / for…in)
Used before its lineundefinedReferenceErrorReferenceError
Declared twiceAllowedSyntaxErrorSyntaxError
Top-level variable becomes window propertyYesNoNo
Use it in new codeNoWhen reassigningBy default
A comparison of var, let and const across six questions. Reassign later: var yes, let yes, const no. Scope: var the whole function, let and const the nearest block. Loop callbacks: var all share one variable, let and const get a fresh variable each iteration. Use before declaring: var gives undefined, let and const throw ReferenceError. Declare twice: var silently allowed, let and const SyntaxError. Verdict: var avoid in new code, let when you must reassign, const by default

Modernizing old code

Converting var code is mostly mechanical. Take this function:

1function summarize(prices) {
2  var total = 0;
3  var count = prices.length;
4  for (var index = 0; index < count; index++) {
5    total += prices[index];
6  }
7  var average = total / count;
8  return "Total " + total + ", average " + average.toFixed(2);
9}
10
11console.log(summarize([3.5, 4.25, 2]));
Total 9.75, average 3.25

Go through each var: is it ever reassigned after its declaration? total is (+=) and so is index (++): they become let. count and average never are: they become const.

1function summarize(prices) {
2  let total = 0;
3  const count = prices.length;
4  for (let index = 0; index < count; index++) {
5    total += prices[index];
6  }
7  const average = total / count;
8  return `Total ${total}, average ${average.toFixed(2)}`;
9}
10
11console.log(summarize([3.5, 4.25, 2]));
Total 9.75, average 3.25

Same output, and now each declaration tells the reader whether the value changes. (The template literal with backticks is another ES6 improvement.)

Your turn: practice

Try these before peeking at the answers.

Exercise 1: Predict the output (warm-up). What do the three console.log lines print?

1const colors = ["red"];
2colors.push("green");
3console.log(colors.length);
4
5let count = 1;
6count = count + 1;
7console.log(count);
8
9var shade = "dark";
10if (true) {
11  var shade = "light";
12}
13console.log(shade);

Exercise 2: const or let? Pick a keyword for each variable: a user's birth year, a score that goes up during a game, a list of to-dos you keep adding to, and the result of Math.max(...numbers).

Exercise 3: Fix the buttons. This code is supposed to report "Clicked button 0", "Clicked button 1" and "Clicked button 2", but every handler reports button 3. Fix it.

1const handlers = [];
2for (var buttonNumber = 0; buttonNumber < 3; buttonNumber++) {
3  handlers.push(() => console.log("Clicked button " + buttonNumber));
4}
5handlers.forEach((handler) => handler());

Exercise 4: Spot the error. What happens when this runs, and why?

1function greet() {
2  console.log(message);
3  const message = "Hi!";
4}
5greet();

Exercise 5: Modernize (challenge). Convert every var to const or let, choosing const whenever you can, and make sure the output doesn't change.

1var words = ["tree", "sky", "banana", "sun"];
2var longest = "";
3for (var index = 0; index < words.length; index++) {
4  var word = words[index];
5  if (word.length > longest.length) {
6    longest = word;
7  }
8}
9console.log("Longest word: " + longest);

Quick quiz

  1. Can you push into an array declared with const?
  2. Where does a variable declared with let inside an if block exist?
  3. Why does a var loop with setTimeout print the same number three times?
  4. What do you get when you use a var before its declaration line? And a let?
  5. Which keyword should you start with?

Answers

Exercise 1: push changes the array's contents, which const allows; count is reassigned, which let allows; and the second var shade isn't a new variable at all, because var ignores the if block and simply reassigns the same shade:

1const colors = ["red"];
2colors.push("green");
3console.log(colors.length);
4
5let count = 1;
6count = count + 1;
7console.log(count);
8
9var shade = "dark";
10if (true) {
11  var shade = "light";
12}
13console.log(shade);
2
2
light

With let shade both times, the inner one would be a separate variable living only in the block, and the last line would print dark.

Exercise 2: Birth year: const (it never changes). Game score: let (score += 10 reassigns it). To-do list: const (push changes the contents, not the variable). Math.max(...numbers): const (it's computed once).

Exercise 3: Change var to let, so every iteration gets its own buttonNumber:

1const handlers = [];
2for (let buttonNumber = 0; buttonNumber < 3; buttonNumber++) {
3  handlers.push(() => console.log("Clicked button " + buttonNumber));
4}
5handlers.forEach((handler) => handler());
Clicked button 0
Clicked button 1
Clicked button 2

Exercise 4: It throws, because message is used inside its temporal dead zone, before its const line:

1function greet() {
2  try {
3    console.log(message);
4  } catch (error) {
5    console.log(`${error.name}: ${error.message}`);
6  }
7  const message = "Hi!";
8}
9greet();
ReferenceError: Cannot access 'message' before initialization

With var message, it would print undefined instead and hide the mistake. The fix is to declare the variable before using it.

Exercise 5: words is never reassigned, so it's const (we only read it). longest changes inside the loop, so it's let. index is reassigned by index++: let. word gets a fresh variable on every iteration and is never reassigned within one: const.

1const words = ["tree", "sky", "banana", "sun"];
2let longest = "";
3for (let index = 0; index < words.length; index++) {
4  const word = words[index];
5  if (word.length > longest.length) {
6    longest = word;
7  }
8}
9console.log("Longest word: " + longest);
Longest word: banana

Even tidier: for (const word of words) removes the index altogether.

Quiz: (1) Yes: const stops you from reassigning the variable, not from changing the array's contents. (2) Only inside that block, between its curly braces, from its declaration line on. (3) There's only one var variable for the whole loop, and the callbacks run after the loop ends, when it's already 3. (4) A var gives undefined; a let throws ReferenceError: Cannot access … before initialization. (5) const.

Cheat sheet

SituationUse
A value you set onceconst
An array or object you add to or change insideconst
A counter, total or flag that you reassignlet
A classic for (… ; … ; index++) loop indexlet
A for…of loop variableconst
A value chosen by an if / elselet (or const with the ? : operator)
Contents that must never changeconst plus Object.freeze
New codeNever var

Wrapping up

const and let aren't just new spellings of var; they fix its biggest traps:

  • Start with const, and switch to let only when you reassign with =.
  • const locks the variable, not the value: arrays and objects can still change inside.
  • let and const are block-scoped; var leaks out of blocks into the whole function.
  • Loops with let give each iteration its own variable, fixing the "all my callbacks see 3" bug.
  • Using let or const too early throws an error (the temporal dead zone) instead of handing you undefined.
  • Redeclaring is an error with let and const.
  • Speed is identical: about 1.3 nanoseconds per loop iteration for all three in our test.

New to the language? Read What Is the JavaScript Programming Language?. For the details, see MDN's pages on const, let and var.

Learn more about JavaScript with Eloquent JavaScript.

Thanks for reading!

Know someone who would enjoy this? Send it their way.

More articles

Read next

Programming 6 min read

C++ std::vector: Size, Capacity, and Safe Growth

Learn how std::vector grows in C++: the difference between size and capacity, when reserve helps, why resize is different, and how growth can invalidate references.

nbkhope
Programming 5 min read

How to Handle Errors in Go: A Hands-On Guide

Learn Go's everyday error pattern with runnable examples: return an error, check it, add context with %w, and recognize a missing file with errors.Is.

nbkhope

Comments

Be kind and specific. Comments with links from new accounts appear after a moderator reviews them.