RizTech Academy logo
RizTech Academy
Functions, Scope and ClosuresLesson 2 of 525 min

Scope and hoisting

The last lesson ended on a puzzle: a function declaration can be called from above the line that defines it, and a const arrow cannot. The answer explains several things at once — including the var leak from module 2 and an error message that reads like a contradiction.

Scope is where a name is visible

const shopName = 'Anna Tiffins';

function greet() {
  const message = 'Namaste';
  console.log(`${message} from ${shopName}`);
}

greet();
console.log(message);
Namaste from Anna Tiffins
ReferenceError: message is not defined

shopName is at the top level, so everything can see it. message lives inside greet, so nothing outside can.

Scope goes inwards only. An inner scope can see outward; an outer scope cannot see in. This is not a restriction to work around — it is what makes a function safe to use without reading it. Its internals cannot collide with yours.

Scopes nest as deeply as you like, and a lookup walks outwards until it finds the name or runs out:

const rate = 80;

function week() {
  const days = 7;

  function describe() {
    console.log(`${days} days at ₹${rate}`);
  }

  describe();
}

week();
7 days at ₹80

describe finds days one level out and rate two levels out.

This is lexical scope: what a name means is decided by where the code is written, not by where it is called from. That sentence is the foundation of the next lesson, so it is worth sitting with.

Shadowing

An inner name with the same spelling hides the outer one:

const rate = 80;

function special() {
  const rate = 60;
  console.log(rate);
}

special();
console.log(rate);
60
80

The outer rate is untouched. Shadowing is legal and occasionally useful, and it is also a good way to confuse yourself. If two things in view have the same name and different meanings, rename one.

Block scope, again

Module 2 showed that let and const are scoped to the nearest braces and var is scoped to the whole function. That is the same rule as above, applied to blocks rather than functions:

function check(plates) {
  if (plates > 0) {
    const status = 'active';
  }
  console.log(status);
}

check(3);
ReferenceError: status is not defined

Declare it outside the block if you need it outside the block. With var this "works", which is precisely the problem — the name escapes and collides.

Hoisting

Now the puzzle. This runs:

console.log(orderTotal(3, 80));

function orderTotal(plates, rate) {
  return plates * rate;
}
240

Before running your code, JavaScript scans the scope and sets up every declaration it finds. Function declarations are set up completely — name and body — so they work from anywhere in their scope. This is called hoisting, which is a slightly misleading name: nothing physically moves, the declarations are simply processed first.

var is hoisted too, but only the name:

console.log(plates);
var plates = 3;
undefined

Not an error — undefined. The name existed from the start of the scope; the value only arrived at line two. A variable that is mysteriously undefined at the top of a function and correct at the bottom is this, and it is a genuinely nasty bug because nothing complains.

The temporal dead zone

let and const are hoisted as well — but reaching them early is an error rather than undefined:

console.log(plates);
let plates = 3;
ReferenceError: Cannot access 'plates' before initialization

Read that message carefully, because it is doing you a favour. It does not say "plates is not defined". It says the name exists and you are too early. The gap between the start of the scope and the declaration line is called the temporal dead zone, and it exists on purpose: turning a silent undefined into a loud error is the whole improvement.

Those two messages are worth telling apart:

Message Means
x is not defined No such name anywhere in scope. Usually a typo.
Cannot access 'x' before initialization The name exists; you used it above its let/const. Move the use down, or the declaration up.

This is also why a const arrow cannot be called early:

orderTotal(3, 80);
const orderTotal = (plates, rate) => plates * rate;
ReferenceError: Cannot access 'orderTotal' before initialization

The function is a value assigned to a const, and the const is in its dead zone. Nothing to do with arrows — a const holding a number behaves identically.

What to do about all this

Declare things before you use them. Then hoisting never affects you and you never have to reason about it. The knowledge is for reading other people's code and for understanding the two error messages, not for writing clever code.

Declaration Hoisted? Before its line
function f() {} Name and body Works
var x Name only undefined
let x / const x Name only, in the dead zone ReferenceError
class C {} Name only, in the dead zone ReferenceError

Check your work

A name declared inside a function is invisible outside it — ReferenceError: message is not defined. Scope goes inwards only.

An inner function can read names from every enclosing scope. describe prints 7 days at ₹80 by finding days one level out and rate two.

Shadowing prints 60 then 80. The inner const rate hides the outer one inside that function and leaves it untouched outside.

const inside an if block is not visible after the block — ReferenceError: status is not defined. With var it would be visible, which is the leak from module 2.

Calling a function declaration before its line works and prints 240. Declarations are hoisted whole.

console.log(plates) above var plates = 3 prints undefined, not an error. The name was hoisted, the value was not.

console.log(plates) above let plates = 3 gives ReferenceError: Cannot access 'plates' before initialization — a different message from plates is not defined, and the difference tells you whether you have a typo or an ordering problem.

A const arrow called before its line fails the same way, because it is a const in its dead zone, not because it is an arrow.

The show() puzzle prints undefined. var rate is hoisted to the top of the function, so at the console.log the name exists with no value yet. The outer rate, if there is one, is not consulted — the local declaration wins for the whole function, even above its own line. Two fixes:

function show() {
  var rate = 80;
  console.log(rate);
}
function show() {
  console.log(rate);
  let rate = 80;
}

The first moves the line and prints 80. The second changes the keyword and turns the silent undefined into ReferenceError: Cannot access 'rate' before initialization — which does not print 80 either, but tells you the truth instead of a plausible lie. That is the trade the second fix is making, and it is usually the one you want.

Practice

  1. Write a function with a local const and try to read it from outside. Read the error.

  2. Nest three functions and have the innermost print a name from the outermost. Confirm it works, then move that name inside a sibling function and watch it break.

  3. Shadow an outer rate with an inner one. Print both and confirm the outer is unchanged.

  4. Declare a const inside an if block and use it afterwards. Read the error. Then fix it by declaring it outside with let.

  5. Call a function declaration from above its definition. Then convert it to a const arrow and watch the same call fail.

  6. Produce both error messages on purpose. First notAThing for is not defined, then a let used above its line for Cannot access ... before initialization. Say out loud what each one is telling you to change.

  7. Print a var above its assignment and get undefined. Then convert it to let and get an error instead. Decide which you would rather have at 11pm.

  8. Harder. This prints undefined rather than 80. Work out why, then fix it in two different ways — one that moves a line, one that changes a keyword.

    function show() {
      console.log(rate);
      var rate = 80;
    }
    show();
    

Next: closures — what happens when an inner function outlives the scope it came from. This is the idea people find hardest, and it follows directly from lexical scope.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship