Topic 3.5
Hoisting and the Temporal Dead Zone
In one line
Before running a scope, JavaScript first registers every declaration in it: function declarations arrive complete, var variables start as undefined, and let, const and class exist but can't be touched until their line runs; that forbidden stretch is the temporal dead zone.
Think of it like this
A teacher taking the register before class. Before any lesson starts, the teacher reads the whole class list, so every name is known from the first minute (that's hoisting). Some students are already sitting at their desks with their books open (function declarations: ready to go). Some desks have a name card but an empty chair (var: the name exists, the value is undefined). And some students are marked "arriving at 10 o'clock: don't call on them before then" (let and const: calling on them early is an error). The time before they arrive is the temporal dead zone.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Hoisting
- Names declared in a scope exist from the start of that scope, before their line runs.
- Creation phase
- The setup pass where the engine creates every declared name before running any statement.
- Execution phase
- The pass that runs the statements, top to bottom.
- Initialise
- Give a variable its first value.
varstarts asundefined;let/constonly when their line runs. - Temporal dead zone (TDZ)
- The time between the start of a scope and a
let/const/classline running, when touching that name is an error. - ReferenceError
- The error thrown when a name doesn't exist, or exists but is still in its TDZ.
Step by step
01Look at the code as written
This code uses hello and score before their lines. Predict each line before reading on.
console.log(hello()); // ?
console.log(score); // ?
var score = 10;
function hello() {
return "ready early";
}02See it the way the engine prepares it
The creation phase registers both names first: hello with its whole body, score as undefined. Then the statements run. This rewritten version behaves exactly the same:
function hello() { // creation phase: complete function
return "ready early";
}
var score; // creation phase: undefined
console.log(hello()); // "ready early"
console.log(score); // undefined
score = 10; // the assignment stays in place03Swap var for let
With let, the early read is no longer a silent undefined: it's an error that points straight at the mistake.
console.log(score); // ReferenceError: Cannot access 'score' before initialization
let score = 10;04Picture the dead zone on a timeline
The zone starts when the scope is entered and ends when the declaration line runs.
05Remember it's about time
A function may mention a let declared later in the file. What matters is that the call happens after the let line has run.
function show() {
return "level " + level; // fine, if called later
}
let level = 3;
show(); // "level 3"Try it yourself
- 1
Predict before you run
In the first example, move
function hello()to the very top. Does any output change? Now change it toconst hello = function () { ... }placed at the bottom: predict the new first line of output. - 2
Shrink the dead zone
In the second example, move
let item = "pen";above thetry. What does the second line of output become? - 3
Rename to fix the shadow
In the third example, rename the inner variable to
innerX. Predict all three lines that change.
Code & diagrams
`sayHi` exists early (it's a `var`), but holds `undefined` until its assignment runs, so calling it is a TypeError, not a ReferenceError.
Console
(nothing was printed)Expected console output
declarations are ready early
undefined
10
TypeError: sayHi is not a function
hi`typeof` is safe on names that don't exist at all, but even `typeof` throws for a name in its TDZ.
Console
(nothing was printed)Expected console output
level is 3
ReferenceError: Cannot access 'item' before initialization
ReferenceError: Cannot access 'later' before initialization
ReferenceError: Cannot access 'sayBye' before initialization
byeInside the block, the inner `let x` is hoisted to the top of the block, so `x` already means the inner one. `f(5)` doesn't need `a`'s default, so `b` is never read early.
Console
(nothing was printed)Expected console output
outer
ReferenceError: Cannot access 'x' before initialization
inner
ReferenceError: Cannot access 'b' before initialization
6Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Read a let before its line
Write console.log(total); let total = 5; at the top of a script.
Break #2
Rely on var's undefined
Write if (!loaded) { console.log("loading..."); } var loaded = true;.
Myth vs fact
Myth
Hoisting physically moves code to the top of the file.
Fact
Nothing moves. The engine creates every declared name in a setup pass before running the scope's statements.
Myth
let and const are not hoisted.
Fact
They are hoisted (the name is reserved for the whole scope), but not initialised. That's exactly why the TDZ exists and why the shadowing example throws instead of using the outer variable.
Myth
typeof never throws.
Fact
It's safe for names that aren't declared anywhere, but throws a ReferenceError for a let/const/class name in its TDZ.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
In spec terms,
varand function bindings are created and initialised during FunctionDeclarationInstantiation / GlobalDeclarationInstantiation, while lexical bindings are created uninitialised; the TDZ is simply an uninitialised binding. - ▸
TDZ checks have a small run-time cost when the engine can't prove a variable is initialised (for example inside closures); V8 removes most of them during optimisation, so this never matters in practice.
- ▸
classdeclarations have a TDZ likelet: you can't use a class above its declaration, unlike constructor functions written as function declarations. - ▸
Hoisting is per scope: a function declared inside another function is hoisted to the top of that function, not to the global scope. Function declarations inside blocks are block-scoped in strict mode, with legacy web-compatibility rules in sloppy mode (Annex B).
Remember this
- 1
JavaScript runs each scope in two passes. First, a setup pass (often called the creation phase) scans the scope and creates every declared name. Then the execution phase runs the statements top to bottom. Because names are created before any line runs, it *looks as if* declarations were moved (hoisted) to the top of their scope. Nothing actually moves in your file; it's a description of the setup pass.
- 2
Function declarations are hoisted with their body: the function is fully usable from the first line of its scope. That's why you can call
hello()abovefunction hello() { ... }. This is deliberate and useful: you can put the main flow at the top of a file and the helper functions below it. - 3
**
vardeclarations are hoisted and initialised toundefined**. The assignment stays where you wrote it. So reading avarbefore its line givesundefinedinstead of an error, a silent bug:console.log(score); var score = 10;printsundefined. A function expression stored in avarfollows the same rule, so calling it early throwsTypeError: sayHi is not a function(the variable exists, but holdsundefined). - 4
**
let,constandclassdeclarations are hoisted too (the name is reserved for the whole scope), but they are not initialised. From the start of the scope until the line with the declaration runs, the variable is in the temporal dead zone (TDZ)**. Any access in that zone, reading, writing, eventypeof, throwsReferenceError: Cannot access 'x' before initialization. This turns thevarsilent bug into a loud, early error. - 5
The TDZ is about time, not position in the file (that's what "temporal" means). A function written above a
letmay still use that variable, as long as the function is called after theletline has run. Only accessing it while the program is still before that line throws. - 6
Hoisting also explains a confusing shadowing case:
let x = 1; { console.log(x); let x = 2; }throws instead of printing1. The innerlet xis hoisted to the top of the block, so inside the block the namexalready means the inner variable, which is still in its TDZ. Default parameters have a TDZ too: infunction f(a = b, b = 1),bis read before it's initialised. Habit that avoids all of this: declare before you use, putconst/letat the top of their block, and preferconstandletovervar.
Explain it without notes
What is hoisting? Describe what happens to function declarations, var, and let/const.
What is the temporal dead zone, and why was it designed this way?
Why does calling a function expression stored in var before its line give a TypeError, while one stored in const gives a ReferenceError?
Why does let x = 1; { console.log(x); let x = 2; } throw?
Practice
Predict the output: console.log(a); var a = 5; console.log(a);
Fix this code without removing any line, only by moving one: console.log(double(4)); const double = n => n * 2;
Write a short example showing that a function written above a let can still use it when called afterwards.
Trade-offs
- ↔
Calling hoisted function declarations before they appear lets you write "main flow first, helpers below", but readers must know about hoisting; some style guides forbid use-before-define for clarity.
Done when you can
I can explain hoisting as a setup pass, not code moving.
I know what function declarations,
var,letandconsteach hold before their line.I can recognise a TDZ error and fix it.
I understand why the TDZ is about time, not position.
I declare variables before using them.