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.
- 01Replay creation vs executionSee hoisting as the creation phase of an execution context.
- 02Predict early readsTell function declarations, var, let, const, class, and missing names apart.
- 03Fix timing bugsReorder code and declarations so TDZ and early-call errors disappear.
Hoisting is the creation phase
STEP THROUGHIn the Execution contexts lesson, you met two phases: a creation phase, then an execution phase. Hoisting is the visible result of that creation phase. Before the first line executes, JavaScript scans the scope and prepares declared names.
The tricky part is that different declarations arrive in different conditions. Function declarations arrive ready. var arrives with undefined. let, const, and class names exist, but they are uninitialized until execution reaches their declaration line. That uninitialized window is the temporal dead zone, or TDZ.
Hoisting does not move your code. It means declarations are registered during creation. The result depends on the declaration kind: ready function, var as undefined, or a TDZ binding that throws if read too soon.
Imagine a teacher taking attendance before class starts. Some students are ready with notebooks open. Some are in the room but empty-handed. Some are present but asleep. And some names are not on the roster at all. Hoisting is JavaScript’s attendance pass before execution begins.
- In real life: The teacher reads the roster before class starts
- In JavaScript: Creation phase registers declared names before line 1
- In real life: A prepared student can answer right away
- In JavaScript: Function declarations are callable immediately
- In real life: A student is present but empty-handed
- In JavaScript:
varexists with valueundefined - In real life: A student is present but asleep
- In JavaScript:
let,const, andclassare in the TDZ - In real life: A name not on the roster
- In JavaScript: An undeclared name is
not defined
Where the analogy stops: A teacher can wake a student gently. JavaScript cannot: reading a TDZ binding throws immediately. Also, the engine does not literally keep a classroom roster; this is a mental model for the creation phase.
Try the replay. Line 2 can read a var, a let, or a missing name. The outcomes are real JavaScript behavior, and the tests for this lesson run the snippets in Node’s vm to prove them.
Creation phase is JavaScript taking attendance before line 1. Change line 2, then predict whether it prints a value or throws.
script
console.log(greet());console.log(score);var score = 7;let status = "ready";console.log(status);Function hoisting: declarations arrive ready
A function declaration is the friendliest form for early calls. During creation, JavaScript creates the function object and stores it in the function’s name. That is why this works:
console.log(double(4));
function double(n) {
return n * 2;
}The body is not executed during creation. It is merely attached to the name. Execution still starts at line 1, calls double, enters the function body, and returns 8.
In modern strict-mode code, a function declaration inside a block is block-scoped. Avoid relying on old sloppy-mode block-function behavior; it exists mostly for web compatibility and can differ between environments. This course uses strict, modern patterns.
This is useful for readability. Many teams put the main story first and helper declarations below it. That works when the helpers are declarations, not expressions assigned to variables.
var hoisting: known, but undefined
CONTRASTvar is older than let and const. It is hoisted and initialized to undefined during creation. The assignment stays exactly where you wrote it.
console.log(score); // undefined
var score = 7;
console.log(score); // 7This is one reason modern JavaScript usually prefers Variables: let, const & var. var can hide bugs by producing a value instead of failing fast. If you expected 7, undefined is still a bug; it is just a quieter bug.
var is like a reserved parking space. The name is already in the lot, but until the car arrives, the space is empty. Reading it early tells you the space exists and is empty: undefined.
- In real life: The sign says Ada has a space
- In JavaScript: The
varname exists during creation - In real life: The car has not arrived
- In JavaScript: The value is
undefinedbefore assignment - In real life: The car arrives later
- In JavaScript: The assignment line stores the real value
Where the analogy stops: A parking sign is visible to people. JavaScript bindings are not visible objects; they are how scope lookup resolves names.
The TDZ for let, const & class
STEP THROUGHlet, const, and class declarations are hoisted too, but not initialized. From the start of the scope until execution reaches the declaration line, the binding is in the temporal dead zone. “Temporal” means it is about time, not simply text position.
console.log(theme); // ReferenceError
let theme = "dark";
console.log(typeof Widget); // ReferenceError
class Widget {}Class declarations behave like let here: the name is known, but reading it before the class declaration runs throws. The class itself is covered in Class basics later; for now, remember that class declarations are TDZ bindings.
TDZ is about time, not text position. Step through the call that is written early but happens late.
script
return secret;}let secret = "initialized";console.log(readLater());typeof notDeclared returns "undefined" because no binding exists. But typeof later before let later throws, because a binding does exist and is in the TDZ. That is one of the sharpest differences between “undeclared” and “not initialized yet.”
Default parameters can have a TDZ too. Parameters are initialized left to right, so function f(a = b, b = 1) reads b before b has been initialized. Swap them to function f(b = 1, a = b) and it works.
Function expressions and arrows depend on the variable
INTERACTIVEFunction declarations are special. A function expression or arrow function is a value created when its assignment line executes. The variable that will hold that value follows the variable’s own hoisting rules.
Choose which form creates double, then predict line 1: value, TypeError, or ReferenceError.
script
console.log(double(4)); return n * 2;}The punchline: var double = function... gives you an early undefined, so calling it throws TypeError. const double = () => ... gives you a TDZ binding, so even reading it for the call throws ReferenceError. The earlier Function expressions & callbacks lesson introduced the syntax; this lesson explains why the timing differs.
undefined vs not defined
These phrases sound similar but mean different things. undefined is a real JavaScript value. “Not defined” is part of a ReferenceError message saying scope lookup did not find a binding.
| Case | Early read result | Why |
|---|---|---|
| Function declaration | Callable function | The binding and function body are prepared during creation. |
var score before assignment | undefined | The binding is created and initialized to undefined; assignment happens later. |
| Declared but not assigned | undefined | A real binding exists, but no value has been stored yet. |
let, const, or class before initialization | ReferenceError | The binding exists but is uninitialized: the TDZ. |
| Never declared | ReferenceError: name is not defined | No binding was found in the scope chain. |
var fn = function... called early | TypeError | The name exists as undefined; calling undefined is not possible. |
Sort the cases by what happens. This is the quickest way to check if you are distinguishing ReferenceError from TypeError and a real undefined value.
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);
Place each early-read snippet into the bucket that matches its real outcome.
Where you’ll use this
Hoisting shows up whenever code is organized for humans. You might put helper declarations below the main flow, split setup across a file, or debug a stack trace that points to a line before a declaration.
console.log(renderGreeting("Ada"));
function renderGreeting(name) {
return "Hello, " + name;
}Best practice is still simple: declare variables near the top of the scope where you use them, initialize const immediately, and do not depend on early var reads. Hoisting knowledge is not a license to be clever; it is a debugging tool.
Common misconceptions
“Hoisting moves my code to the top.”
No. Source order is unchanged. Creation prepares bindings; execution still runs statements in order.
“Only functions are hoisted.”
Function declarations are the easiest to see, but var, let, const, and class names are all registered during creation.
“TDZ means the line is physically above the declaration.”
TDZ is about when execution reads the binding. A function written above a let is fine if it is called after the let line runs.
“typeof never throws.”
It is safe for undeclared names, but not for TDZ bindings.
“ReferenceError and TypeError are interchangeable.”
A ReferenceError means the name lookup failed or hit TDZ. A TypeError means a value was found, but the operation is invalid, such as calling undefined.
Practice: predict and fix timing bugs
5 EXERCISESPredict the first value logged by this program.
console.log(makeLabel("Ada"));
var prefix = "Hi";
function makeLabel(name) {
return prefix + ", " + name;
}The function declaration is ready, so the call runs. Inside it, prefix is the hoisted var value undefined, so the console prints undefined, Ada.
Reorder the code so it uses the arrow function after initialization.
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 value is created by the assignment line. Calling it after that line prints Hi, Ada.
Which one throws: typeof notDeclared or typeof later before let later = 1?
typeof notDeclared; // "undefined"
typeof later; // ReferenceError before:
let later = 1;typeof on an undeclared name returns "undefined". typeof on a TDZ binding throws, so the TDZ case is the one that fails.
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 helper can stay below or above, but the call must happen after taxRate is initialized. Then 100 + 100 * 0.08 prints 108.
Fix the default parameters so 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 have their own initialization order. With b = 1 first, a = b can safely read 1, so the function returns 2.
Quiz: check your understanding
7 QUESTIONSEvery choice explains the outcome. For error questions, focus on which kind of error and why.
Question 1 of 7What is hoisting, in the mental model from this lesson?
Choose an answer to see the explanation.
Question 2 of 7What does the function declaration snippet print?
Read the code, then predictconsole.log(say()); function say() { return "hello"; }Choose an answer to see the explanation.
Question 3 of 7What does the early var snippet 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 7What does the delayed let read 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 is the best description of
typeof laterbeforelet later = 1?Choose an answer to see the explanation.
Question 7 of 7Which early call throws TypeError rather than ReferenceError?
Choose an answer to see the explanation.
Key takeaways
- Hoisting is the creation phase preparing declared names, not text moving.
- Function declarations are callable before their declaration line runs.
varis initialized toundefined; its assignment still runs later.let,const, andclassare in the TDZ until initialized.undefinedis a value; “not defined” means no binding was found.
Remember the one-liner.
Hoisting is JavaScript taking attendance during creation: functions ready, var undefined, TDZ bindings present but unsafe to read.
Up next: Memory: primitives vs objects.