Learn by reading through in order

Scope and Hoisting — Where Variables Can Be Read, and When

Scope decides where a variable can be read, and hoisting registers declarations before code runs. Covers shadowing, the TDZ, and why to skip var.

If you use a value calculated inside curly braces from outside them, execution stops. It also stops when a line reads a setting that's declared further down. Both errors come down to where a name can be used, and from what point.

This article covers scope, which decides where a declared name can be read, and hoisting, which registers declarations before the code runs.

Variables That Only Work Inside Braces — Block Scope

Say you want the cart screen to show how much more the customer needs to spend to get free shipping. You can calculate the shortfall inside the if that checks for free shipping, but if you try to display that value outside the braces, execution stops with a ReferenceError.

Scope (the range that decides where a name can be read) is marked off by curly braces. The range of one pair of braces is called block scope, and a variable declared with const or let can only be read inside the innermost block scope that encloses its declaration.

const cartTotal = 4800;            // Outermost level, not inside any braces

function checkShipping(total) {
  const freeLine = 5000;           // Only inside checkShipping's braces

  if (total < freeLine) {
    const shortage = freeLine - total;  // Only inside the if's braces
    console.log(`${shortage} yen to go`);  // 200 yen to go
  }

  // Variables declared outside can be read from inside
  console.log(cartTotal);          // 4800
  // console.log(shortage);        // ReferenceError: shortage is not defined
}

checkShipping(cartTotal);
Which Variables You Can Read from Outside
Outermost level (not inside any braces)
  • const cartTotal = 4800 — readable from anywhere in the file
  • checkShipping(cartTotal) — calls the function from the outermost level
checkShipping's braces
  • const freeLine = 5000 — readable only inside these braces
  • The outer cartTotal is readable here too
  • console.log(shortage) — not readable once you're outside the if, so ReferenceError
The if's braces
  • const shortage = 200 — readable only inside these braces
  • Both freeLine and cartTotal can be read
Code inside can read values from outside, but a value created inside the if can't be read once you leave its braces. Reading only works one way: from inside to outside.

The outermost level, not inside any braces, is the global scope, and variables there can be read from anywhere. A function body is a pair of braces too, so you can't read freeLine from outside the function. The range marked off by a function's braces is called function scope.

Show members how many points they still need for the next rank. points and nextRankLine are already declared.

① Declare remaining with let to hold the points still needed, in a spot where it can be read from outside the if.

② Only when the points fall short, assign the remaining points to remaining inside the if's braces.

③ Outside the if, display it in the form "N points to the next rank".

(If it runs correctly, an explanation will appear.)

JavaScript / TypeScript Editor

Run code to see output

The Inner Declaration Wins — Shadowing

When you add a function that calculates prices at a reduced tax rate, you might declare a variable inside it called taxRate, the same name as the outer one. If you don't know which value is used, you can't track down why a result came out wrong.

If you declare a name in an inner scope that already exists in an outer one, the inner declaration is used inside those braces. This is called shadowing (an inner declaration hiding an outer variable with the same name), and it happens with both if braces and function braces.

const taxRate = 0.1;                 // Tax rate set on the outside

function withTax(price) {
  return Math.floor(price * (1 + taxRate));
}

function withReducedTax(price) {
  const taxRate = 0.08;              // Same name, declared inside the function
  return Math.floor(price * (1 + taxRate));
}

console.log(withTax(1000));          // 1100
console.log(withReducedTax(1000));   // 1080
console.log(taxRate);                // 0.1 (the outer one hasn't changed)
Same Name, Different Value Depending on Where You Read It
const taxRate= 0.1 (outer)Read insidewithTaxRead insidewithReducedTaxRead outsidethe functionsNo innerdeclaration: 0.1Finds theinner 0.08Uses theouter 0.1Returns 1100Returns 1080Displays 0.1
The same name taxRate finds a different declaration depending on where you read it. An inner declaration doesn't overwrite the outer value.

A name is looked up starting from the innermost braces around the line that reads it. Inside withReducedTax, the search stops as soon as it finds 0.08, so the outer 0.1 is never read. Code where one name means a different value in each function is hard to follow, so in your own code, give them distinct names such as reducedTaxRate.

Without const, You Assign to the Outer Variable

If you write taxRate = 0.08; inside a function without const, no new variable is created — it's an assignment to the outer taxRate. If the outer one is a const, execution stops with a TypeError, but if it's a let, the value gets overwritten, and even a later call to withTax(1000) returns 1080.

Swap the price heading on a product page, but only during a sale. label and isSale are already declared.

① When isSale is true, declare label as "Sale price" inside the if's braces, and display it.

② Display label outside the if.

③ Write a second if where the inner name is changed to saleLabel, and display it in the form "Regular price → Sale price".

JavaScript / TypeScript Editor

Run code to see output

Unreadable Until the Declaration Line — Hoisting and the TDZ

Say you want to group settings like a point rate or a shipping fee on lines below the code that uses them. As you saw in the functions article, a function declaration can be called even when it's written further down, but reading a let or const setting before its declaration line stops execution with a ReferenceError.

Before JavaScript starts running a scope, it registers the names of the declarations in it. This is called hoisting (declarations being registered before the code runs). A function declaration registers its name and body together, so you can call it from lines above the declaration. let and const register only the name; the value goes in when the declaration line runs.

// The function body reads pointRate when it's called
function calcPoint(amount) {
  return Math.floor(amount * pointRate);
}

// Calling it here means the pointRate declaration line hasn't run yet
// console.log(calcPoint(3200));
// → ReferenceError: Cannot access 'pointRate' before initialization

let pointRate = 0.05;              // Settings grouped further down

console.log(calcPoint(3200));      // 160 (readable, since the declaration line has run)
When a let Value Becomes Readable
The scopestarts runningOnly pointRate'sname is registeredUntil the letline is reachedCalling calcPoint→ ReferenceErrorRuns let pointRate= 0.05Calls after thisreturn 160
Until the let line runs, the value isn't there, even when a function written above reads it. Whether you can read it depends on the order things run, not on where you wrote them.

The stretch from entering a scope until the let declaration line runs is called the TDZ (Temporal Dead Zone — the period when a name can't be read until its declaration line runs). If the error message says is not defined, the name isn't in scope at all. If it says before initialization, the name exists, but you read it before its declaration line ran.

Show the amount left until free shipping and the points earned on the order confirmation screen. orderTotal is already declared.

① Declare the free-shipping threshold freeLine with let as 5000, and on the line below it, display "N yen to free shipping".

② First write the line that displays "Points earned: N", then on the line below it, declare the point rate pointRate with let as 0.05.

③ Check how many lines get displayed and which error stops execution.

JavaScript / TypeScript Editor

Run code to see output

Registered Early as undefined — Why Not to Use var

You'll see variables declared with var in older articles and libraries, and if you copy code that works, var can come along with it. But var differs from const and let in two ways: where the variable is valid, and what you get when you read it before its declaration.

A var declaration is registered early too, but undefined goes in at the same time. So referencing it on a line above the declaration doesn't cause an error — execution just moves on with undefined. It isn't limited to the braces, either: it's valid throughout the enclosing function.

function checkStock(orderCount) {
  // Reading it before the declaration doesn't stop anything; it holds undefined
  console.log(stock);              // undefined

  var stock = 12;
  console.log(stock);              // 12

  if (orderCount <= stock) {
    var status = "Ready to ship";      // Declared with var
    let note = "Ship from warehouse A"; // Declared with let
  }
  // var is valid throughout the function, so it's readable outside the if
  console.log(status);             // Ready to ship
  // console.log(note);            // ReferenceError: note is not defined
}

checkStock(3);
var Isn't Confined by Braces
var statusassigned in ifLeaves theif's bracesStill inside thefunction: readable"Ready to ship"is displayedlet noteassigned in ifLeaves theif's bracesnote only existsinside the ifStops withReferenceError
Even when the assignment happens inside the same if, a var value can be read on lines after the braces end. With let, execution stops on the line that reads it outside its range.

A function has only one variable per var name, so declaring the same name again with var inside an if doesn't shadow anything — it overwrites the same variable. The table below shows, for each kind of declaration, what's stored when the name is registered before the code runs, and what you get when you read it above the declaration line.

DeclarationRegistered Before RunningRead Above the Declaration
var stock = 12The name and undefinedContinues with undefined
let stock = 12The name only (no value)Stops with ReferenceError
const stock = 12The name only (no value)Stops with ReferenceError

Switching var to let Makes Some Lines Stop

When you replace var with let in old code, lines that read the variable before its declaration, or outside the if, stop with a ReferenceError. Those lines were reading undefined, or a value from other braces, all along. To fix them, move the declaration above the line that reads it, into braces that also enclose that line.

See how a var variable behaves depending on where it's declared. cartTotal is already declared.

① First write the line that displays "Amount due: N yen", subtracting discount from cartTotal, then below it declare discount with var as 300.

② Below that, display it again.

③ If the total is 5000 yen or more, declare discount again with var as 800 inside an if.

④ Outside the if, display "Discount: N yen".

JavaScript / TypeScript Editor

Run code to see output
QUIZ

Knowledge Check

Answer each question one by one.

Q1What happens if you display unit on the line after if (true) { const unit = "yen"; }?

Q2Near the top of the file, you define a function show that returns rate. You then call it on a line below let rate = 0.1;. What does it return?

Q3If you write console.log(count); on a line above var count = 3;, what is displayed?