Command Palette

Search for a command to run...

Hectal
PHASE 3Beginner ~24 min· topic 5 of 9

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. var starts as undefined; let/const only when their line runs.
Temporal dead zone (TDZ)
The time between the start of a scope and a let/const/class line 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.

hoist.jswhole filejavascript
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:

hoist-explained.jswhole filejavascript
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 place

03Swap var for let

With let, the early read is no longer a silent undefined: it's an error that points straight at the mistake.

tdz.jswhole filejavascript
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.

Picture the dead zone on a timelinediagram
Rendering diagram…

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.

time.jswhole filejavascript
function show() {
  return "level " + level; // fine, if called later
}
let level = 3;
show(); // "level 3"

Try it yourself

  1. 1

    Predict before you run

    In the first example, move function hello() to the very top. Does any output change? Now change it to const hello = function () { ... } placed at the bottom: predict the new first line of output.

  2. 2

    Shrink the dead zone

    In the second example, move let item = "pen"; above the try. What does the second line of output become?

  3. 3

    Rename to fix the shadow

    In the third example, rename the inner variable to innerX. Predict all three lines that change.

Code & diagrams

Function declarations and var are hoisted differently New tab

`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
The temporal dead zone ES2015+ New tab

`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
bye
The shadow that throws ES2015+ New tab

Inside 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
6

Break 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.

terminal
$ Console
── what you'll see ──
Uncaught ReferenceError: Cannot access 'total' before initialization

Break #2

Rely on var's undefined

Write if (!loaded) { console.log("loading..."); } var loaded = true;.

terminal
$ Console
── what you'll see ──
loading... (printed every time, with no error)

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, var and 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.

  • ▸

    class declarations have a TDZ like let: 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. 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. 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() above function 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. 3

    **var declarations are hoisted and initialised to undefined**. The assignment stays where you wrote it. So reading a var before its line gives undefined instead of an error, a silent bug: console.log(score); var score = 10; prints undefined. A function expression stored in a var follows the same rule, so calling it early throws TypeError: sayHi is not a function (the variable exists, but holds undefined).

  4. 4

    **let, const and class declarations 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, even typeof, throws ReferenceError: Cannot access 'x' before initialization. This turns the var silent bug into a loud, early error.

  5. 5

    The TDZ is about time, not position in the file (that's what "temporal" means). A function written above a let may still use that variable, as long as the function is called after the let line has run. Only accessing it while the program is still before that line throws.

  6. 6

    Hoisting also explains a confusing shadowing case: let x = 1; { console.log(x); let x = 2; } throws instead of printing 1. The inner let x is hoisted to the top of the block, so inside the block the name x already means the inner variable, which is still in its TDZ. Default parameters have a TDZ too: in function f(a = b, b = 1), b is read before it's initialised. Habit that avoids all of this: declare before you use, put const/let at the top of their block, and prefer const and let over var.

Explain it without notes

01

What is hoisting? Describe what happens to function declarations, var, and let/const.

02

What is the temporal dead zone, and why was it designed this way?

03

Why does calling a function expression stored in var before its line give a TypeError, while one stored in const gives a ReferenceError?

04

Why does let x = 1; { console.log(x); let x = 2; } throw?

Practice

01

Predict the output: console.log(a); var a = 5; console.log(a);

02

Fix this code without removing any line, only by moving one: console.log(double(4)); const double = n => n * 2;

03

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, let and const each 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.