Hoisting, var and the Temporal Dead Zone

Before JavaScript runs a scope it notes every declaration in it, which is why a function can be called above the line that makes it, why var gives undefined there, and why let and const stop with an error instead.

Journeyman · JavaScript · Updated

After this lesson you can

  • Predict what happens when code uses a variable or function before the line that declares it
  • Explain how var differs from let and const, and rewrite older var code with let or const

Key takeaways

  • Before a scope runs, JavaScript notes every declaration in it. This is called hoisting.
  • A function declaration is ready from the first line of its scope, so you can call it above the line that makes it.
  • A var used before its line gives undefined. A let or const used before its line stops with a ReferenceError: the temporal dead zone.
  • var belongs to the whole function, not to the block it's written in, and it can be declared twice without an error.
  • In new code, use const, and let when the value must change. Read var in older code as "belongs to the whole function".

Search the web for almost any JavaScript question and you'll find answers written with var instead of let and const. You'll meet it in tutorials, in a class's sample code and in the code of almost any company that has had a website for a while. var follows older rules, and those rules explain a puzzle you may have already noticed: you can call a function on line 1 that isn't written until line 20, but a let used one line too early stops the program. This lesson explains both, so you can read older code and know what to write instead.

Before the first line runs

Before the Starlight Arcade opens each morning, the manager walks the floor and puts a name card on every spot where a machine belongs: Space Blaster goes here, Skee-Ball goes there. Some machines are already plugged in; some are still in their boxes. But from the moment the doors open, the staff know every machine the arcade has.

JavaScript does the same. Before it runs the first line of a scope, it reads the whole scope and notes every declaration in it: every function, var, let and const. Only then does it run the lines, top to bottom. This is called hoisting, from "hoist", to lift up: it behaves as if the declarations were lifted to the top of their scope. Nothing really moves in your file; JavaScript knows the names early.

What each name holds at that early moment depends on how it was declared, and that's the rest of this lesson.

Function declarations are ready early

A function declaration is a function made with the word function and a name, as its own statement. It's plugged in before the doors open: you can call it from any line in its scope, even above the line that makes it.

console.log(tokensFor(5));

function tokensFor(dollars) {
  return dollars * 4;
}
20

That's handy: a file can start with the main steps of a program and keep the helper functions at the bottom, out of the way.

var: a name card with nothing on it yet

A variable made with var is hoisted too, but only its name. Its value arrives when its line runs. Used earlier than that, it holds undefined, the value JavaScript gives a variable that has no value yet:

console.log(`Tokens: ${tokens}`);

var tokens = 20;

console.log(`Tokens: ${tokens}`);
Tokens: undefined
Tokens: 20

No error, no warning: the program carries on with undefined, and the trouble shows up later, somewhere else. A bug that hides like that is hard to find.

let and const: the temporal dead zone

let and const are hoisted as well, so JavaScript knows their names from the start of the block. But it won't let any line touch them until their own line has run. "Temporal" means "to do with time", and the stretch of time from the start of the block until the declaration runs is called the temporal dead zone (often shortened to TDZ). Use the variable inside it, and the program stops with a clear message:

console.log(`Tickets: ${tickets}`);

let tickets = 50;
C:\arcade\early.js:1
console.log(`Tickets: ${tickets}`);
                        ^

ReferenceError: Cannot access 'tickets' before initialization

"Initialization" means giving a variable its first value. Compare this message with the one for a name that doesn't exist at all, is not defined: here JavaScript is saying "I know this name, but you're too early." A loud error on the right line is much kinder than a quiet undefined, which is one reason let and const replaced var.

A function made with const and an arrow follows the same rule: const tokensFor = (dollars) => dollars * 4; can't be called above its line. Only function declarations are ready early.

Try it. Save the tickets code as early.js and run it with node early.js. Then change let to var and run it again. Which do you find easier to fix: the error or the undefined?

var belongs to the whole function

The other big difference: var ignores blocks. A var made inside an if or a loop belongs to the whole function around it (or to the whole file, at the top level). In the house picture from Scope: Where a Variable Can Be Seen, a var never stays in the closet; it always moves out into the room.

function checkPrize(tickets) {
  if (tickets >= 100) {
    var prize = 'giant stuffed panda';
  }
  console.log(`Prize: ${prize}`);
}

checkPrize(150);
checkPrize(40);

for (var round = 1; round <= 3; round++) {
  console.log(`Round ${round}`);
}
console.log(`After the loop, round is ${round}`);
Prize: giant stuffed panda
Prize: undefined
Round 1
Round 2
Round 3
After the loop, round is 4

With let or const, prize and round would belong to their blocks, and both lines outside the blocks would stop with a ReferenceError. With var, prize exists everywhere in the function (holding undefined when the if didn't run), and round is still there after the loop, holding the 4 that ended it. var also lets you declare the same name twice in one scope, with no error, where let stops with Identifier 'playerName' has already been declared.

Why var is still around, and what to write instead

Until 2015, var was the only way to make a variable in JavaScript. That year's version of the language (ES2015, often called ES6) added let and const, and nobody could remove var without breaking millions of websites that still use it. So both stay.

var let const
Belongs to the whole function its block its block
Used before its line undefined ReferenceError (the TDZ) ReferenceError (the TDZ)
Declared twice in one scope allowed an error an error
Given a new value later allowed allowed an error

When you read older code, read each var as "this belongs to the whole function". When you write or update code, use const by default, and let when the value must change:

function totalTickets(games) {
  let total = 0;
  for (let i = 0; i < games.length; i++) {
    const tickets = games[i] * 2;
    total = total + tickets;
  }
  return total;
}

console.log(totalTickets([10, 25, 5]));
80

This was var total, var i and var tickets before. total changes, so it becomes let; i changes every time around the loop, so let; and tickets gets one value each time around, so const.

Try it. Write a function with var in an if block, as checkPrize does, and change the var to let. Run it and read the error. Then move the line that uses the variable inside the block, where it belongs.

Check yourself

2 questions to try on the lesson page.

So what

Hoisting explains why a function works above its line and a let doesn't, and var's rules explain a lot of surprising older code. You'll read var for years; write const and let, and the temporal dead zone will catch your too-early mistakes for you.

Key terms

Cheat sheets

Related