← Fundamentals
Guided lesson 7 questions · TypeScript only

TypeScript var, hoisting, and the temporal dead zone

A var exists before its line and holds undefined. A let or const exists too, but reading it there throws.

Every var, let, and const in a function or block is created when that function or block starts. A var starts as undefined; a let or const has no value until its line runs, and reading it before then throws a ReferenceError.

Written for TypeScript.

Before you begin

Why this matters

You will read code that uses var, and code that calls functions before their lines. If you know what each name holds before its line runs, you can predict a printed undefined, explain an error that names a variable, and spot functions made in a loop that all print the same value.

What this helps you reason about

  • Predict whether reading a name early gives undefined, a compile error, or an error while the program runs.
  • Spot functions made in a var loop that all see the loop’s last value.
  • Say which functions can be called above the line that defines them.

In most languages, using a variable above the line that declares it is an error. JavaScript, which TypeScript runs as, has two answers. The older keyword var gives undefined, and the program continues. let and const throw an error. This lesson shows why both happen, how the compiler catches some of these reads before the program runs, and which bugs the older answer still causes in code you will read.

Already know this? The short version
  • Every declaration exists from the start of its function or block (hoisting). A var starts as undefined; a let or const has no value until its line runs, and reading it before then throws ReferenceError: Cannot access 'x' before initialization (the temporal dead zone).
  • TypeScript refuses a direct read above a declaration, but not a read inside a function that is called too early; that one fails while the program runs.
  • A var belongs to the whole function: it can be read after the if that declares it, and declaring it twice is allowed. A for loop with var has one counter for every pass, so functions made in it all see the last value; let gives each pass its own.
  • A function declaration can be called above its line; a function expression or arrow function stored in a variable cannot. The section “What goes wrong with var and hoisting in real programs?” covers handlers made in a loop, a var leaking out of an if, a setting read before it is assigned, and names that are also browser globals.

Go to the final check· Go to the practice tickets

Words in this lesson (8)
var
The older JavaScript keyword for declaring a variable. A var belongs to the whole function around it, not to a block.
undefined
A special JavaScript value that means “no value yet”. A let variable declared without a value holds undefined.
hoisting
The effect that a name exists from the start of its scope, as if its declaration were moved to the top: the whole function for a var, the block for let and const. A var starts as undefined, a function declaration starts with its body, and a let or const starts with no value.
temporal dead zone (TDZ)
The time between the start of the block or file that holds a let or const and the line that declares it. Reading or assigning the name in that time throws a ReferenceError.
arrow function
A function written as a value, without a name, such as () => "stall " + i. The part before => lists its parameters, and the part after it is the value it returns.
closure
A function together with the variables from the surrounding code that it uses. Those variables last as long as the function does.
function declaration
A statement that starts with function and a name, such as function price(kg: number) { … }. It is created with its body before any line of its scope runs.
function expression
A function written as a value and stored in a variable, such as const price = function (kg: number) { … } or an arrow function. The variable holds the function only after its line runs.

Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.

01 / Reading early

What does a var hold before its line runs?

Scope and shadowing showed that let and const belong to the nearest block, while var belongs to the whole function around it. This lesson asks what each one holds before the line that declares it has run.

When a function starts, JavaScript first creates every var declared anywhere in its body, and gives each one the value undefined. This effect is called hoisting: the function behaves as if each declaration had been moved to its top. The value does not move. A var gets its value only when the line with = runs.

So the function below reads plums before its declaration, gets undefined, and continues. The type number | undefined means “a number, or undefined”. Without it, TypeScript refuses the first read with Variable 'plums' is used before being assigned. The compiler catches many early reads like this one, but not all of them, as the next sections show.

main.ts
function weigh(): void {
  console.log("plums:", plums); // plums: undefined
  var plums: number | undefined = 6;
  console.log("plums:", plums); // plums: 6
}

A var exists from the start of its function and holds undefined until its line runs.

Coming from Python: Python raises UnboundLocalError when a function reads a local before assigning it. A JavaScript var gives undefined instead, and the program continues with that value.

Precisely: nothing moves

Hoisting is a teaching name, not a step in the ECMAScript specification. Before a function body runs, the engine creates a record of its names (FunctionDeclarationInstantiation). Each var name is added and set to undefined at that moment. The same happens for the top level of a file. The source text is never rearranged.

Experiment

Read a var before its line

When weigh starts, every var in its body is created at once and holds undefined. Line 3 does not create plums; it only assigns 6. A let is created without a value, and TypeScript refuses a read that comes above its declaration in the same block.

Why it matters When a var prints undefined, look for its = line further down the function.

Predict both lines. Then change var plums to let plums on line 3 and predict again.

function weigh(): void {
  console.log("plums:", plums);
  var plums: number | undefined = 6;
  console.log("plums:", plums);
}

weigh();
What to look for

At the first stop inside weigh, on line 2, weigh’s frame already lists plums = undefined, although line 3 has not run. The run prints plums: undefined. At line 4, plums shows 6, and the run prints plums: 6. After the edit, nothing runs: the compiler reports Block-scoped variable 'plums' used before its declaration. for line 2. The stepper sometimes stops on the same line twice; that is normal for this debugger.

Expected output

plums: undefined
plums: 6

02 / Dead zone

Why does reading a let early throw an error?

let and const are created at the start of their block too, but with no value at all. Until the line that declares them runs, they are in the temporal dead zone: the time between the start of the block and the declaration. Reading or assigning the name in that time throws a ReferenceError, an error that stops the program unless the code handles it. The message is Cannot access 'plums' before initialization. The stepper shows such a variable as not initialized yet.

TypeScript reports a direct read above the declaration before the program runs: Block-scoped variable 'plums' used before its declaration. It cannot follow every call, though. Below, showPlums reads plums. The compiler accepts the program, because it does not track when showPlums is called. The error comes while the program runs.

The dead zone is about time, not about where the code is written. showPlums is written above let plums, and it works when it is called after that line has run.

main.ts
function showPlums(): void {
  console.log("plums:", plums);
}

showPlums(); // ReferenceError
let plums = 4;

A let or const read before its line has run throws a ReferenceError; a var read in the same way gives undefined.

Coming from Java: Java refuses every use of a local variable above its declaration when it compiles. TypeScript refuses the direct read too, but a read inside another function waits until the program runs.

Precisely: other names with a dead zone

A class declaration behaves like let: it exists from the start of its block and has no value until its line runs.

Experiment

Read a let through a function

plums exists from the start of the file, but it has no value until line 5 runs. A read in that time throws. The compiler cannot know when showPlums will be called, so it does not report the read.

Why it matters Whether a read of a let is safe depends on when the code runs, not on where it is written.

Predict what prints. Then swap lines 5 and 6, so showPlums(); comes before let plums = 4;, and predict again.

function showPlums(): void {
  console.log("plums:", plums);
}

let plums = 4;
showPlums();
What to look for

The first stop is line 5, because the function declaration needs no step. The top level lists plums = not initialized yet. At line 6, plums shows 4. Inside showPlums, plums shows 4, and the run prints plums: 4. After the edit, the compiler accepts the program. At line 5 plums still shows not initialized yet; inside showPlums it shows the same, and the run stops with ReferenceError: Cannot access 'plums' before initialization, at showPlums (line 2).

Expected output

plums: 4

03 / Function scope

Where does a var belong?

A var belongs to the whole function, not to the block where it is written. A var written inside an if is created when the function starts, like every other var, and code after the if can read it. When the if does not run, the var still exists, and it holds undefined.

Each call of the function creates its own vars, so a value from an earlier call is never left over. Inside one call, though, there is one variable for the whole body.

Declaring the same var twice in one function is allowed, and both lines mean one variable. let refuses this: two let declarations of count in one block give Cannot redeclare block-scoped variable 'count'.

main.ts
function label(kg: number): string {
  if (kg > 10) {
    var note: string | undefined = "heavy";
  }
  return kg + " kg " + note; // "3 kg undefined" for 3
}

A var written inside a block is one variable for its whole function, readable before and after the block.

Precisely: var outside every function

Outside every function, a var belongs to the whole file. In a browser script, it also becomes a property of the global object, window, which already has properties such as name and status. The TypeScript handbook recommends let and const for new code, and the no-var rule of the ESLint code checker reports every var.

Experiment

Read a var after its if

var note belongs to the whole of label, so it exists before the if and after it. Each call of label creates its own note, holding undefined, and only the if gives it a value. A let note belongs to the if block, so line 5 is outside its scope.

Why it matters A var declared inside a block can be read after the block, where it may still hold undefined.

Predict both lines. Then change var note to let note on line 3 and predict again.

function label(kg: number): string {
  if (kg > 10) {
    var note: string | undefined = "heavy";
  }
  return kg + " kg " + note;
}

console.log(label(12));
console.log(label(3));
What to look for

At the first stop of each call, on line 2, the frame lists note = undefined. In label(12), the if runs, and at line 5 note shows "heavy". In label(3), the if is skipped, note still shows undefined at line 5, and the run prints 12 kg heavy, then 3 kg undefined. After the edit, nothing runs: the compiler reports Cannot find name 'note'. for line 5.

Expected output

12 kg heavy
3 kg undefined

04 / Loops

Why do functions made in a var loop all see the last value?

A function is a value, so it can be stored in a variable or an array. () => "stall " + i is an arrow function: a function with no name that takes no parameters and returns the value after =>. callers.push(f) adds f to the end of the array callers. stalls.length is the number of elements, and stalls[i] reads the element at position i, counting from 0. callers[0]() reads the first function in callers and calls it. The type (() => string)[] means an array of such functions.

A function made inside other code keeps using the variables around it, not copies of their values. A function together with the variables it uses is called a closure. The function reads each variable when it runs, so it sees the value the variable has at that moment.

In a for loop with var i, there is one i for the whole function or file. Every arrow function made in the loop uses that one i. They run after the loop, when i is 3, so each reads stalls[3], which does not exist and gives undefined. With let i, the loop makes a new i for each pass, holding that pass’s number, and each function keeps the i of its own pass.

main.ts
const stalls = ["fish", "bread", "cheese"];
const callers: (() => string)[] = [];
for (var i = 0; i < stalls.length; i++) {
  callers.push(() => "stall " + i + ": " + stalls[i]);
}
console.log(callers[0]()); // stall 3: undefined

Functions made in a for loop share a var counter; with let, each pass of the loop has its own counter.

Coming from Go: Before Go 1.22, a for loop’s variable was shared by every pass, the same trap as var. Since Go 1.22 each pass has its own variable, like let.

Coming from Python: Python’s lambda: i in a loop also sees the last i; Python needs a default argument such as lambda i=i: i, where JavaScript needs only let.

Precisely: how let makes a counter for each pass

For a for loop whose header declares let, the specification copies the counter into a new variable at the start of each pass (CreatePerIterationEnvironment), and i++ then changes that new copy. for (const stall of stalls) also gives each pass its own stall. Code written before let existed got the same effect by passing i to a function that was called at once.

Experiment

Make functions in a var loop

With var, the file has one i. All three functions use that one variable, and they run after the loop, when it holds 3. stalls[3] does not exist, so it gives undefined. With let, each pass of the loop has its own i, and each function keeps the i of the pass that made it.

Why it matters When functions made in a loop all show the last value, look for a var counter.

Predict both lines. Then change var i to let i on line 3 and predict again.

const stalls = ["fish", "bread", "cheese"];
const callers: (() => string)[] = [];
for (var i = 0; i < stalls.length; i++) {
  callers.push(() => "stall " + i + ": " + stalls[i]);
}
console.log(callers[0]());
console.log(callers[1]());
What to look for

At the first stop, on line 1, the top level lists i = undefined, while stalls and callers are not initialized yet. Line 3 sets i to 0, 1, 2, and 3, and callers fills with three arrow function () values. When callers[0]() runs, the stepper enters a frame named arrow function (line 4), and that frame lists i = 3. The run prints stall 3: undefined twice. After the edit, the top level lists no i after the loop. The first function’s frame lists i = 0, the second’s i = 1, and the run prints stall 0: fish and stall 1: bread.

Expected output

stall 3: undefined
stall 3: undefined

05 / Functions

Can you call a function above its line?

A function declaration is a statement that starts with function and a name: function price(kg: number) { … }. It is hoisted together with its body, so it can be called from any line of its scope, including lines above it. That is why many programs put their helper functions at the bottom of a file.

A function expression is a function written as a value and stored in a variable, such as const price = function (kg: number) { … }, or an arrow function. Only the variable is hoisted, under the rules above. With const or let, a call above the line is a compile error, or a ReferenceError when the call happens inside another function. With var, the variable holds undefined, and calling it throws TypeError: price is not a function.

main.ts
console.log(price(3)); // 12

function price(kg: number): number {
  return kg * 4;
}

A function declaration can be called above its line; a function stored in a variable can be called only after its line has run.

Precisely: function declarations inside blocks

In strict mode, which TypeScript output uses, a function declaration written inside a block belongs to that block. Older scripts without strict mode follow extra rules for this case, described in Annex B of the specification, and browsers differ in the details. Write such helpers at the top level of a function, or as const arrow functions.

Experiment

Call a function above its declaration

A function declaration is created, with its body, before any line of its scope runs. A function expression is a value, and it is stored in price only when its line runs. With const, price is in the temporal dead zone on line 1, and the compiler can see that.

Why it matters Call a function declaration from anywhere in its scope. A function stored in a variable can be called only after its line has run.

Predict what prints. Then turn the declaration into a function expression: replace line 3 with const price = function (kg: number): number {, and predict again.

console.log(price(3));

function price(kg: number): number {
  return kg * 4;
}
What to look for

At the first stop, on line 1, the top level already lists price = function price(kg), before line 3. The stepper enters price with kg = 3, and the run prints 12. After the edit, nothing runs: the compiler reports Block-scoped variable 'price' used before its declaration. for line 1, and a second message, Variable 'price' is used before being assigned.

Expected output

12

06 / In production

What goes wrong with var and hoisting in real programs?

Each of these bugs comes from code that runs in a different order than it is written.

Functions made in a loop. Click handlers created in a for loop with var all see the counter’s last value. Use let, or for…of with const.

A var that leaks out of an if. Code after the block can read the var. When the if did not run, the var holds undefined, and a label such as 3 kg undefined reaches the screen. TypeScript reports many of these reads, Variable 'note' is used before being assigned, but not once the type includes undefined.

A setting read before it is assigned. A file’s top-level code runs from top to bottom. If it calls a function that reads a setting declared lower in the file, a const throws a ReferenceError at start-up, and a var gives undefined with no error, as below. Declare the settings first, and put the calls that start the work last. Two modules that import each other can fail in the same way.

A name that is also a browser global. A TypeScript file with no import or export is a script: its top-level names share one scope with browser globals such as name and status. let name = "Ana" then gives Cannot redeclare block-scoped variable 'name'. In a browser, a top-level var in a script also becomes a property of window. Add an import or an export, so the file is a module with its own top level.

main.ts
var market = "Harbour market";
showBoard(); // Harbour market opens at undefined
var opensAt = 7;

function showBoard(): void {
  console.log(market + " opens at " + opensAt);
}

Use let and const, declare settings before the code that starts the work, and make each file a module.

Precisely: what the compiler checks at the top level

TypeScript reports a direct read of a top-level let or const above its declaration, as inside a function. It does not report a read inside a function that the top level calls too early, which is the case above. A read of a var whose type includes undefined is never reported.

Experiment

Read a setting before it is assigned

Top-level code runs from top to bottom. showBoard is called on line 2, before line 3 assigns opensAt. A var gives undefined there and the wrong text goes out. A const throws at the read, and the message names the setting.

Why it matters Declare every setting before the first line that starts work, and prefer const, which fails loudly when the order is wrong.

Predict what the board prints. Then change both var keywords to const and predict again.

var market = "Harbour market";
showBoard();
var opensAt = 7;

function showBoard(): void {
  console.log(market + " opens at " + opensAt);
}
What to look for

At the first stop, on line 1, the top level lists market = undefined, opensAt = undefined, and showBoard. At line 2, market shows "Harbour market" and opensAt still shows undefined. Inside showBoard, opensAt is undefined, and the run prints Harbour market opens at undefined, with no error. After the edit, the compiler accepts the program. The first stop lists both settings as not initialized yet, and the run stops in showBoard with ReferenceError: Cannot access 'opensAt' before initialization.

Expected output

Harbour market opens at undefined

07 / Try it

Can you predict what each name holds?

Follow the counter and the jar through the loop, then decide what town holds when stallSign is called.

main.ts
const tasters: (() => string)[] = [];
for (var n = 1; n <= 2; n++) {
  let jar = n;
  tasters.push(() => "olive jar " + jar + " of " + n);
}
console.log(tasters[0](), "/", tasters[1]());
console.log(stallSign());

function stallSign(): string {
  return "olives from " + town;
}

var town: string | undefined = "Kalamata";

What are the two output lines?

Reveal the reasoning

olive jar 1 of 3 / olive jar 2 of 3, then olives from undefined. jar is a let inside the loop body, so each pass has its own jar, 1 and then 2. n is a var, so both functions share one n, which is 3 when the loop ends. stallSign is a function declaration, so it can be called on line 7. It reads town, a var that exists from the start of the file but is assigned only on the last line, so it holds undefined.

Practice tickets

  1. TS-101 · Every buzzer calls an order that does not exist → Free
  2. TS-102 · Full-price vegetables print with a reduced sticker → Sign in · Pro
  3. TS-103 · The cherry receipt fails for every sale → Sign in · Pro

Keep exploring

Bits, bytes, and binary →

Primary references