Hoisting & the temporal dead zone
Understand why function declarations work before their line, why var reads as undefined, why let, const, and class have a temporal dead zone, and how undefined differs from not defined.
- 01Watch names get set upStep through the creation phase, when JavaScript sets up every name before line 1 runs.
- 02Predict early readsSay what happens when code reads a function, a var, a let, a const, a class, or a missing name too early.
- 03Fix timing bugsReorder code so that every name has its value before it is read or called.
Names are set up before line 1 runs
STEP THROUGHIn the Execution contexts lesson, you saw that JavaScript runs your code in two phases:
- The creation phase: before any line runs, JavaScript reads through the code and sets up every name it declares.
- The execution phase: JavaScript runs the lines one by one, from top to bottom.
A declaration is the code that creates a name, like var total, let theme, or function greet(). Hoisting is what you see because of the creation phase: some names can be used on lines above their declaration.
Each kind of declaration starts in a different state:
- A function declaration is ready to call.
- A
varname holdsundefined, the value that means "nothing here yet". - A
let,const, orclassname exists, but you can't read it yet. It gets its first value only when its own line runs.
The time before that line runs is called the temporal dead zone, or TDZ. "Temporal" means "about time". Reading the name during that time throws an error.
console.log(total);
var total = 5;
console.log(total);Line 1 reads total before line 2 gives it a value, so it prints undefined. Line 2 stores 5. Line 3 prints 5. The name was set up early. The value came only when line 2 ran.
Be careful: the first undefined is not a real total. It is a bug that stays quiet. With let or const instead of var, line 1 would throw an error. The error points you straight at the line that reads too early.
Hoisting does not move your code. It means JavaScript sets up every declared name before line 1 runs. What an early read gives you depends on the kind of name:
- A function declaration gives a ready function.
- A
vargives the valueundefined. - A
let,const, orclassthrows an error, because the name is in its TDZ.
A teacher takes attendance before the lesson starts. Some students are ready, with notebooks open. Some are in the room but have brought nothing. Some are in the room but asleep. And some names are not on the class list at all.
- In real life: The teacher reads the class list before the lesson starts
- In JavaScript: JavaScript sets up every declared name before line 1
- In real life: A student is ready, with a notebook open
- In JavaScript: A function declaration can be called right away
- In real life: A student is there, but has brought nothing
- In JavaScript: A
varname exists and holdsundefined - In real life: A student is there, but asleep
- In JavaScript: A
let,const, orclassname is in its TDZ - In real life: A name that is not on the class list
- In JavaScript: A name with no declaration is "not defined"
greet is ready to call. The var name score holds undefined. The let name status has a box, but you can't read it until line 4 runs.Now try the replay. Its code has a function, a var, and a let. You choose what line 2 reads: the var, the let, or a name that was never declared. The results are what real JavaScript does. The tests for this lesson run each version to check them.
Before line 1, JavaScript sets up every declared name. Change line 2, then guess: does it print a value or throw?
script
console.log(greet());console.log(score);var score = 7;let status = "ready";console.log(status);Function declarations are ready early
A function declaration is a line that starts with the word function and gives the function a name, like function double(n). During the creation phase, JavaScript makes the whole function and puts it in that name. So you can call it on a line above the declaration:
console.log(double(4));
function double(n) {
return n * 2;
}Line 1 calls double(4). That works, because double was set up before line 1 ran. The function body, return n * 2, does not run during the creation phase. It runs only when line 1 calls the function. So line 1 prints 8.
A block is code inside curly braces, like the body of an if. Strict mode is the modern set of rules you turn on with "use strict". In strict mode, a function declared inside a block belongs to that block only.
Older non-strict code has a different, confusing rule for this. It exists only so that old websites keep working, and it can differ between browsers. Don't rely on it. This course writes strict, modern code.
"use strict";
if (true) {
function greet() {
return "inside";
}
console.log(greet());
}
console.log(typeof greet);Line 6 is inside the if block. It calls greet() and prints inside. Line 8 is outside the block. There is no greet out there, so typeof greet prints undefined.
typeof gives the kind of a value as text, like "number" or "function". For a name that doesn't exist, it gives "undefined" instead of throwing an error.
if (true) {
function greet() {
return "inside";
}
}
console.log(typeof greet);This is the same code without "use strict". When Node.js runs it as a plain script, line 6 prints function. The old rule copied greet outside the block. Don't depend on this. Keep a block's functions inside the block. If you need a helper outside the block, declare it outside on purpose.
Function declarations also help you organize code. Many teams put the main steps at the top of a file and the helper functions below. That works only when the helpers are function declarations, not functions saved in variables.
var names start as undefined
CONTRASTvar is the oldest way to declare a variable. It is older than let and const. During the creation phase, a var name is set up and given the value undefined. The assignment, like = 7, stays on its own line. It runs only when that line is reached.
console.log(score); // undefined
var score = 7;
console.log(score); // 7Line 1 prints undefined, because line 2 hasn't run yet. Line 2 stores 7. Line 3 prints 7.
score exists from the start, but it holds undefined. Line 2 puts 7 in the same box. So line 1 prints undefined, and line 3 prints 7.This is one reason modern code usually uses let and const instead (see Variables: let, const & var). With var, an early read gives a value instead of an error. If you expected 7, undefined is still a bug. It is just harder to notice.
A var name is like a reserved parking space. The sign is there from the start, but the space stays empty until the car arrives. If you look early, you find the space, and it's empty.
- In real life: The sign says this space is for Ada
- In JavaScript: The
varname exists from the creation phase - In real life: Ada's car hasn't arrived yet
- In JavaScript: The value is
undefineduntil the assignment runs - In real life: The car arrives later
- In JavaScript: The assignment line stores the real value
let, const & class: the temporal dead zone
STEP THROUGHlet, const, and class names are also set up during the creation phase. But they get no value at all, not even undefined. A name with no value yet is called uninitialized. It stays that way until its own line runs.
The time from the start of the block until that line runs is the temporal dead zone (TDZ). Any read during that time throws a ReferenceError. That is the error JavaScript throws when it can't use a name. "Temporal" means the TDZ is about when the read happens, not where it is written.
console.log(theme); // ReferenceError
let theme = "dark";
console.log(typeof Widget); // ReferenceError
class Widget {}Line 1 reads theme before line 2 gives it a value, so it throws a ReferenceError, and the program stops there.
theme exists from the start, but it stays empty and locked until line 2 runs. Reading it on line 1 throws a ReferenceError. After line 2, it holds "dark".Lines 4 and 5 show the same rule for a class. Run on their own, they throw too: typeof Widget on line 4 reads the class name before line 5 has run. You'll learn classes in Class basics later. For now, remember that a class name follows the same rule as let.
console.log(label);
import { label } from "./value.mjs";This example only works in a module: a JavaScript file that can use import and export. Line 1 reads label, and line 2 imports it from another file. It still works, and prints ready. Before any line of a module runs, JavaScript loads every import and gives those names their values.
The editor on this page can't run code that is split across two files. So the lesson test runs this exact example with a second file, value.mjs, which exports label.
The TDZ is about time, not about where code is written. In the replay below, a function that reads secret is written above let secret. But it is called after line 4 runs, so the read is safe.
The TDZ is about when a read runs, not where it is written. This function is written early, but called late.
script
return secret;}let secret = "initialized";console.log(readLater());typeof notDeclared gives "undefined", because there is no name notDeclared at all. But typeof later, on a line before let later, throws an error. The name later does exist; it just has no value yet. So "never declared" and "declared, but no value yet" behave differently.
console.log(typeof notOnTheRoster);Nothing in this code declares notOnTheRoster. JavaScript looks for the name, finds nothing, and typeof gives "undefined". This safe answer is only for names that don't exist. It doesn't help with a name that is still in its TDZ:
console.log(typeof later);
let later = 1;Here, line 2 declares later, so the name exists. But on line 1 it has no value yet. typeof still has to read it, so it throws a ReferenceError.
let theme = "light";
{
console.log(theme);
let theme = "dark";
}Line 1 makes an outer theme with the value "light". Lines 2 to 5 are a block with its own let theme on line 4. When a block declares a name that already exists outside, the inner name shadows the outer one: inside the block, only the inner name can be seen.
This starts at the top of the block, not at line 4. So line 3 does not read "light". It reads the inner theme, which has no value yet, and throws a ReferenceError.
This mistake is easy to make in long functions with blocks inside blocks. To fix it, give the inner variable a different name. Or give it its value before the first read. Don't expect the outer value to be used.
Default parameters can have a TDZ too. A default parameter is the value a parameter uses when the caller doesn't pass one, like b = 1. Parameters get their values from left to right. So in function f(a = b, b = 1) {}, a tries to read b before b has a value, and the call throws. Swap them to function f(b = 1, a = b) {} and it works.
Function expressions and arrows follow their variable
INTERACTIVEA function expression is a function written as a value and saved in a variable, like var double = function (n) { … }. An arrow function is a shorter way to write one, like const double = (n) => n * 2.
Either way, the function is made only when its line runs. Until then, the variable follows its own rule: a var holds undefined, and a const is in its TDZ. Pick each version in the replay and watch what line 1 does.
Pick how double is made, then guess what line 1 does: print a value, throw a TypeError, or throw a ReferenceError.
script
console.log(double(4)); return n * 2;}double(4) before line 2 runs. Only the function declaration is ready. The var box holds undefined, and calling undefined is a TypeError. The const box can't be read yet, so the call fails with a ReferenceError.Here is what each version does on line 1:
- Function declaration:
doubleis ready, so line 1 prints 8. varwith a function expression:doubleholdsundefined. Callingundefinedthrows aTypeError. That is the error for using a value in a way that doesn't work.constwith an arrow:doubleis in its TDZ. Just reading it for the call throws aReferenceError.
The earlier Function expressions & callbacks lesson showed how to write these. This lesson explains why they behave differently when you call them early.
undefined vs not defined
These two sound alike, but they mean different things:
undefinedis a real JavaScript value. It means "this name exists, but holds nothing useful yet".- "Not defined" is part of an error message, like
ReferenceError: x is not defined. It means JavaScript looked for the name and found no declaration for it anywhere.
The table below shows what happens when code reads a name too early. Read each row as: "when the name is this kind, an early read gives this, because …"
| What the name is | What an early read gives | Why |
|---|---|---|
| A function declaration | A function you can call. | The whole function was put in the name before line 1 ran. |
var score, read before its assignment | undefined | The name was set up with undefined. The assignment runs later, on its own line. |
A var that was never given a value | undefined | The name exists, but nothing was ever stored in it. |
let, const, or class, read before its line | ReferenceError | The name exists, but it has no value until its line runs. It is in its TDZ. |
| A name that was never declared | ReferenceError: name is not defined | JavaScript looked for the name and found no declaration for it. |
var fn = function …, called early | TypeError | The name holds undefined, and you can't call undefined. |
Now sort the cases yourself. Put each line of code in the group for what it really does. This checks that you can tell a real undefined apart from a ReferenceError and a TypeError.
console.log(score); var score = 1;console.log(add(1, 2)); function add(a, b) { return a + b; }console.log(score); let score = 1;run(); const run = () => 'go';run(); var run = function () {};console.log(notDeclared);
Put each line of code in the group that matches what it really does when it runs.
Where you’ll use this
Hoisting matters whenever you arrange code so that people can read it easily. For example, you might:
- put helper functions below the main code,
- split setup code across a file,
- fix an error whose line number points above a declaration.
console.log(renderGreeting("Ada"));
function renderGreeting(name) {
return "Hello, " + name;
}Line 1 calls renderGreeting("Ada") and prints Hello, Ada. The helper is declared on line 3, below the call. That works because it is a function declaration, so it was ready before line 1 ran.
Still, keep your code simple:
- Declare each variable near the top of the block where you use it.
- Give a
constits value on the same line. - Never depend on reading a
varearly.
Knowing about hoisting helps you find bugs. It is not a reason to write tricky code.
if (true) {
var banner = "Sale";
let secret = "coupon";
}
console.log(banner); // Sale
console.log(typeof secret); // undefinedLines 2 and 3 run inside the if block. A var name belongs to the whole function or script around it, so banner still exists after the block. Line 5 prints Sale.
A let name belongs only to its block, so secret is gone after the closing brace on line 4. Line 6 uses typeof, which is safe for a missing name, so it prints undefined. Because of this difference, take care when you change old var code to let. Check each change; don't swap them all without looking.
The next example uses setTimeout. It runs a function later, after the current code has finished. Each loop below asks for three functions to run later, and each function prints index.
for (var index = 0; index < 3; index++) {
setTimeout(() => console.log("var", index), 0);
}
for (let index = 0; index < 3; index++) {
setTimeout(() => console.log("let", index), 0);
}The first loop prints var 3 three times. All three functions share one index, because a var belongs to the whole file, not to the loop. When the functions finally run, the loop has finished and index is 3.
The second loop prints let 0, then let 1, then let 2. With let, each turn of the loop gets its own index.
This bug shows up on real pages with click handlers and timers. Because the code uses setTimeout, the editor on this page doesn't run it. The lesson test runs it in Node.js and checks all six lines.
const order = "A12";
console.log(order);
// ESLint: no-use-before-define catches this when order is below its use.
function printOrder() {
return order;
}Line 1 gives order its value first. Line 2 prints A12. The helper printOrder on lines 5 to 7 reads order, and it is written after order has its value.
A linter is a tool that checks your code for mistakes before it runs. The ESLint linter has a rule, no-use-before-define, that warns when code uses a name above its declaration. When you see that warning, move the setup up so the order is clear.
Common misconceptions
“Hoisting moves my code to the top.”
No. Your code stays where you wrote it. The creation phase only sets up names. Then the lines run in the order you wrote them.
“Only functions are hoisted.”
No. Function declarations are the easiest to notice. But var, let, const, and class names are all set up in the creation phase too.
“The TDZ means the read is written above the declaration.”
No. The TDZ is about when the read runs. A function written above a let is fine if you call it after the let line runs.
“typeof never throws.”
It is safe for names that were never declared. It still throws for a name in its TDZ.
“ReferenceError and TypeError mean the same thing.”
No. A ReferenceError means JavaScript couldn't use the name: it doesn't exist, or it is in its TDZ. A TypeError means JavaScript found a value, but you can't do that with it, like calling undefined.
When an early read surprises you, ask three questions in order:
- What kind of declaration made the name?
- Has its line run yet?
- Can you do this with the value you found? For example, is it a function you can call?
The answers tell apart a TDZ ReferenceError, a var that holds undefined, and a TypeError from calling something that isn't a function.
- Find the line that reads the name. The error message gives its line number. Don't only look at the declaration below it.
- Check what made the name: a function declaration,
var,let,const,class, animport, or nothing at all. - Move the line that gives the value above the read. Or move the read to a place where the name already has its value.
Practice: predict and fix timing bugs
5 EXERCISESGuess what each program does before you run it. Use the hints if you get stuck.
What does line 1 print? Write the exact text.
console.log(makeLabel("Ada"));
var prefix = "Hi";
function makeLabel(name) {
return prefix + ", " + name;
}The function declaration is ready, so the call works. Inside it, prefix is a var that still holds undefined. So the console prints undefined, Ada.
Line 1 calls an arrow function before it exists. Reorder the lines so the call comes after the function is made. What does the fixed code print?
console.log(format("Ada"));
const format = (name) => "Hi, " + name;// Move the call below the assignment.
const format = (name) => "Hi, " + name;
console.log(format("Ada"));The arrow function is made when its line runs. Calling it after that line prints Hi, Ada.
Which one throws: typeof notDeclared, or typeof later on a line before let later = 1?
typeof notDeclared; // "undefined"
typeof later; // ReferenceError, because of the let below
let later = 1;typeof on a name that was never declared gives "undefined". typeof on a name in its TDZ throws. So the TDZ name is the one that throws.
Move only what you need, so the program prints the total instead of throwing.
console.log(total(100));
const taxRate = 0.08;
function total(price) {
return price + price * taxRate;
}const taxRate = 0.08;
function total(price) {
return price + price * taxRate;
}
console.log(total(100));The function can be above or below the call. What matters is that the call comes after taxRate has its value. Then 100 + 100 * 0.08 prints 108.
Fix the default parameters so that calling total() prints 2.
function total(a = b, b = 1) {
return a + b;
}
console.log(total());function total(b = 1, a = b) {
return a + b;
}
console.log(total());Parameters get their values in order. With b = 1 first, a = b can read 1. So the function returns 2.
Quiz: check your understanding
7 QUESTIONSFor each code question, first ask what kind of declaration made the name. Each choice explains why it is right or wrong.
Question 1 of 7What is hoisting?
Choose an answer to see the explanation.
Question 2 of 7What does this early function call print?
Read the code, then predictconsole.log(say()); function say() { return "hello"; }Choose an answer to see the explanation.
Question 3 of 7What does this early var read print?
Read the code, then predictconsole.log(score); var score = 10;Choose an answer to see the explanation.
Question 4 of 7What happens here?
Read the code, then predictconsole.log(score); let score = 10;Choose an answer to see the explanation.
Question 5 of 7The function is written above the let. What does this code print?
Read the code, then predictfunction readLater() { return secret; } let secret = "ok"; console.log(readLater());Choose an answer to see the explanation.
Question 6 of 7What happens with
typeof lateron a line beforelet later = 1?Choose an answer to see the explanation.
Question 7 of 7Which early call throws a TypeError, not a ReferenceError?
Choose an answer to see the explanation.
Key takeaways
- Hoisting means JavaScript sets up every declared name before line 1 runs. Your code does not move.
- You can call a function declaration on a line above it.
- A
varname starts asundefined. Its assignment still runs on its own line. let,const, andclassnames are in their TDZ until their line runs. Reading them earlier throws.undefinedis a value. "Not defined" means JavaScript found no declaration for the name.
Remember the one-liner.
Before line 1, every name is set up: functions are ready, var holds undefined, and let, const, and class wait for their own line.
Up next: Memory: primitives vs objects.