let, const and block scope
Declare variables the modern way with const and let, understand block scope and the temporal dead zone, and know why var is legacy.
What you will learn
- Choose between const and let for each variable
- Explain block scope and shadowing
- Explain the temporal dead zone and what var hoisting does differently
- Say what const does and does not protect
On this page
A variable is a name for a value. What a variable is, and how assignment differs from comparison, is covered in the fundamentals lesson on variables and assignment. This lesson covers the three ways JavaScript lets you declare one and the scope rules that come with each.
const and let
Modern JavaScript has two declarations you should use, const and let. Both arrived in ES2015.
constcreates a name that cannot be assigned again.letcreates a name that can.
let score = 10;score = score + 5;console.log(score);
const limit = 100;console.log(limit);15100score changes, so it is a let. limit never changes, so it is a const. Assigning to a const stops the program:
const limit = 100;limit = 200;TypeError: Assignment to constant variable.Lesson 15 shows how to catch an error like this one instead of letting it stop the program.
The next example shows that const does not make a value unchangeable. It fixes the binding between the name and the value. If the value is an array (a list) or an object (a record), you can still change what is inside it. push adds an item to the end of a list, and config.retries = 5 replaces one part of a record.
const scores = [10, 20];scores.push(30);console.log(scores);
const config = { retries: 3 };config.retries = 5;console.log(config);[ 10, 20, 30 ]{ retries: 5 }The lesson on copies and references explains why this works. For now, remember what const guards: the name, not the contents.
The working rule is to start with const and change to let only when you find the variable must be reassigned. Readers then know that every const keeps its meaning for the whole scope, and every let is a warning that the value moves.
Updating a variable
A let variable is often updated from its own value. JavaScript has short forms for that. They are called compound assignment operators.
| Short form | Same as |
|---|---|
x += 3 |
x = x + 3 |
x -= 3 |
x = x - 3 |
x *= 3 |
x = x * 3 |
x /= 3 |
x = x / 3 |
x++ and ++x |
x = x + 1 |
x-- and --x |
x = x - 1 |
x++ and ++x both add one, but they differ in the value they produce. The postfix form x++ gives back the old value and then adds one. The prefix form ++x adds one and gives back the new value. The same holds for --.
let count = 5;count += 3;count *= 2;console.log(count);
let n = 1;console.log(n++, n);console.log(++n, n);161 23 3count goes from 5 to 8 and then to 16. In the last two lines, n++ prints the old value 1 and n becomes 2. Then ++n makes n 3 and prints 3. When you use ++ on a line of its own, the two forms do the same thing. Avoid mixing them into larger expressions, because they are hard to read.
Block scope
A block is the code between { and }. A bare pair of braces is a block, and so are the bodies of the if statements, loops and functions that the next lessons cover. Variables declared with let and const exist only inside the block where they are declared.
const outer = "outside";
{ let inner = "inside"; const alsoInner = 42; var leaky = "var ignores blocks"; console.log(outer, inner, alsoInner);}
console.log(leaky);console.log(typeof inner);outside inside 42var ignores blocksundefinedinner and alsoInner are visible inside the braces and nowhere else. typeof inner outside the block gives "undefined" because no such name exists there. (Using typeof on a name that was never declared does not throw. Names in the temporal dead zone, described below, are the exception.)
The var line behaves differently, and the output shows it: leaky is still available after the block. We come back to that below.
A block can also shadow a name from outside it:
let name = "outer name";
{ let name = "inner name"; console.log(name);}
console.log(name);inner nameouter nameDeclaring name again inside the braces creates a new, separate variable that hides the outer one until the block ends. Shadowing is legal, and it is also a common source of confusion. Prefer a different name when the two variables could be mixed up.
The scope rules in general, including lifetime, are in the fundamentals lesson on scope and lifetime. The scope of functions gets its own lesson in the next module.
The temporal dead zone
You cannot use a let or const variable before its declaration line runs. The span between the start of the scope and that line is the temporal dead zone, and touching the variable there throws a ReferenceError.
console.log(later);let later = "now it exists";ReferenceError: Cannot access 'later' before initializationThe message is the one to remember: “Cannot access ‘later’ before initialization”. It tells you the name exists in this scope but has no value yet. That is a bug you want to find early, and the dead zone turns it into an immediate error instead of a quiet undefined. Even typeof later throws in this zone, so typeof is only safe for names that were never declared at all.
var and hoisting
Before ES2015 the only declaration was var. It still works, and you will meet it in older code. It has differences that cause bugs.
varis scoped to the whole function, not the block. Avarinside braces is visible after them, asleakyshowed above.varis hoisted and starts asundefined. The declaration is treated as if it were at the top of its scope, so reading it early givesundefinedinstead of an error.varallows the same name twice in one scope. A secondvar greetingdoes not complain, which hides typos.
console.log(count);var count = 5;console.log(count);
var greeting = "hello";var greeting = "hello again";console.log(greeting);undefined5hello againcount is undefined on the first line, not an error. The second var greeting silently replaces the first. With let or const, that second declaration would be a SyntaxError.
var has one more trap, in loops: a var loop counter is shared by every pass, while a let counter is new on each pass. The loops lesson and the lesson on closures show where that matters.
Use let and const in everything you write. Read var when you meet it, and replace it with const or let when you edit that code.
Common pitfalls
- Treating
constas “frozen”.conststops reassignment only. A list or record declared withconstcan still change inside. - Declaring with
letout of habit. If a variable is never reassigned, make itconst. - Reading a variable above its declaration. That is the temporal dead zone. Move the declaration up.
- Declaring the same name twice in one scope.
let x; let x;is aSyntaxError.var x; var x;is allowed and hides mistakes. - Using a name after its block. With
letandconstit is out of scope, which is usually what you want.
Exercises
This older program uses var throughout. Rewrite it with const and let so that: each name is declared once, visitor exists only inside its braces (print it there), and the program prints Ada and then Hi 15.
var greeting = "Hello";var greeting = "Hi";
{ var visitor = "Ada";}
var total = 10;total = total + 5;
console.log(greeting, visitor, total);Hi Ada 15Input: none. The program above prints Hi Ada 15. Expected output after your fix: Ada, then Hi 15.
Show solution
const greeting = "Hi";
{ const visitor = "Ada"; console.log(visitor);}
let total = 10;total += 5;
console.log(greeting, total);AdaHi 15greeting is declared once and never reassigned, so it is a const. visitor is a const inside the braces and is printed there. total changes, so it stays a let, and total += 5 is the short form of the update.
Predict the four lines this program prints, then run it.
const label = "outer";
{ const label = "inner"; console.log(label);
{ const label = "innermost"; console.log(label); }
console.log(label);}
console.log(label);Input: none. Expected output: inner, innermost, inner, outer.
Show solution
const label = "outer";
{ const label = "inner"; console.log(label);
{ const label = "innermost"; console.log(label); }
console.log(label);}
console.log(label);innerinnermostinnerouterEach pair of braces gets its own label. When a block ends, its label disappears and the next one out becomes visible again.
Declare let a = 1; and let b = 2;. Swap their values with the help of a third variable, then print both on one line.
Input: none. Expected output: 2 1.
Show solution
let a = 1;let b = 2;
const temp = a;a = b;b = temp;
console.log(a, b);2 1a and b change, so they are let. temp holds the old a and is never reassigned, so it is a const. Without temp, the second assignment would have lost the first value.
Predict the three lines this program prints, then run it.
let n = 5;console.log(n++);console.log(++n);console.log(n);Input: none. Expected output: 5, 7, 7.
Show solution
let n = 5;console.log(n++);console.log(++n);console.log(n);577n++ gives the old value, 5, and then n becomes 6. ++n makes n 7 first and gives back 7. The last line shows the final value, 7.
Summary
- Use
constby default andletwhen a variable must be reassigned.+=,-=,++and--update aletvariable in place. letandconstare block-scoped. A name shadowed in an inner block hides the outer one until the block ends.- Using a
letorconstbefore its declaration is aReferenceError(the temporal dead zone). varis function-scoped, hoisted asundefinedand may be declared twice. Treat it as legacy.constfixes the binding, not the contents of the value.
Next: choosing between code paths with if, switch and the conditional operator, in the conditionals lesson.