Lesson 8 · 18 min read

let, const, var and the Temporal Dead Zone

The full picture of the three declaration keywords: every axis they differ on, function scope versus block scope (and what loops do), exactly where the TDZ starts and ends and why it is temporal rather than positional, the places it hides, redeclaration, errors that stop your file before it runs, and the one exception to "typeof is always safe".

On this page

  1. Concept
  2. Key words in plain English
  3. Why this matters
  4. The blueprint vs. the locked room — an analogy
  5. The three keywords side by side
  6. Function scope vs. block scope — and what loops do
  7. The TDZ, precisely: where it starts and where it ends
  8. Why "temporal" — it is about time, not position
  9. The TDZ in disguise: shadowing and default parameters
  10. Redeclaration: var allows it, let and const don't
  11. The parse-time twist: why some errors crash everything
  12. The five error types — full taxonomy
  13. The typeof exception: TDZ breaks the safety net
  14. const: a frozen binding, not a frozen value
  15. Minimizing your own TDZ window — and which keyword to use
  16. Real example — traced line by line
  17. See it live — console & DevTools
  18. Common misconceptions
  19. Interview questions

Concept

Lesson 3 gave you the headline: var, let and const are all hoisted, and the difference is what happens next — var starts as undefined, let/const start locked in the Temporal Dead Zone (TDZ). This lesson is the full picture. Every axis on which the three keywords differ, in one grid. Function scope versus block scope, including the place where it matters most: loops. Exactly which lines the TDZ covers, why it is called temporal rather than positional, and the two places it hides where you would not expect it. Then the parts that confuse even experienced developers: redeclaration, errors that stop your whole file before a single line runs, the five distinct error messages, and the one real exception to “typeof is always safe”.

One mental model carries the whole lesson: a declaration reserves a slot in the memory phase; the keyword decides what the slot starts as, which braces own it, and what you are allowed to do to it afterwards. Everything below is a consequence of those three decisions.

Key Words In Plain English

Short definitions first, so every later section is easier to read.

  • Variable is a named box in memory that holds a value. let age = 7 makes a box called age that holds 7.
  • Binding is the link between a name and its box. A variable is really a binding: the name age points to a value.
  • Scope is the part of the code where a name can be used. Outside its scope, the name does not exist.
  • Block is any code inside a pair of curly braces { }, for example the body of an if or a for.
  • Hoisting is the engine reserving memory for declarations before it runs any line of the code. It does not move your code. It only means the names are known in advance.
  • Initialise means give a binding its first value. Until then it is empty (var gives undefined) or locked (let/const).
  • Temporal Dead Zone (TDZ) is the period of time between a let/const name being reserved and its declaration line running. In that period the name exists but reading or writing it throws an error.
  • Shadowing is when an inner scope declares a name that already exists in an outer scope. Inside, the inner one hides the outer one.
  • Parsing is the engine reading your source text and checking that it is valid JavaScript, before anything runs. Runtime is the time when the code actually runs, line by line.
  • SyntaxError is an error for text that is not valid JavaScript. It is found while parsing. ReferenceError is an error for using a name that cannot be used right now. TypeError is an error for doing something a value does not allow, such as changing a const. These two are found at runtime.
  • typeof is an operator that returns the type of a value as a string, such as "number". For a name that was never declared it returns "undefined" and does not throw.
  • Closure is a function that remembers the variables from the place where it was created, even after that place has finished running.
  • Primitive is a simple value that cannot be changed in place: a number, string, boolean, undefined, null, symbol or bigint. An object (including arrays) is a value with properties that can be changed.
  • Object.freeze is a built-in function that stops an object from getting new, changed or removed properties. It only protects the top level.

Why This Matters

  • “Which keyword should I use?” is answered properly only once you can see all the differences at once — scope, initial value, reassignment, redeclaration and the global object — rather than the one or two you happen to remember.
  • The loop case (for (var i…) versus for (let i…)) is behind the single most famous JavaScript interview question, which Lesson 11 traces in full. This lesson gives you the memory picture it depends on.
  • Redeclaration bugs are common when refactoring — knowing exactly which combinations throw and which don't saves real debugging time.
  • Understanding that some errors are caught before your code even starts running explains why “but the console.log above the error never even printed” happens.
  • The five distinct error types are a favourite interview drill — knowing which one applies to which mistake proves real understanding, not memorized keywords.
  • The typeof TDZ exception is a real pitfall that surprises even experienced developers.

The Blueprint vs. The Locked Room — An Analogy

Two completely different kinds of “it didn't work”

Imagine a building inspector reviewing the hotel's blueprint before construction even starts. If the blueprint itself has a real mistake — say, the same room number used twice on the same floor — the inspector rejects it outright. Construction never begins. Not even the perfectly fine lobby on that same blueprint gets built that day, because the whole plan was thrown out before a single brick was laid.

Now imagine the blueprint was fine, construction went ahead, and the hotel opened. A guest tries a specific room before it's officially ready and finds the door locked — that's the TDZ. Everything else in the hotel is running completely normally; only that one specific room is temporarily off-limits.

One is a plan that never gets approved at all. The other is a working building with one locked door. JavaScript treats these as two entirely different categories of error.

The Three Keywords Side By Side

Before going deep on any one row, here is the whole table. Read it top to bottom once now; the rest of the lesson explains the rows that need explaining.

varletconst
Hoisted?yesyesyes
Value before its lineundefined🔒 TDZ (throws)🔒 TDZ (throws)
Scopefunction (or global)block { }block { }
Must initialise on declaration?nono (defaults to undefined)yes — SyntaxError otherwise
Reassign later?yesyesno — TypeError
Redeclare in same scope?yesno — SyntaxErrorno — SyntaxError
Top-level: property of window?yesnono
Fresh binding per loop iteration?no — one shared slotyesyes (no i++ though)
Memory record (global)Object Environment RecordDeclarative Environment RecordDeclarative Environment Record
Every axis on which the three keywords differ. The first row is the one people get wrong: all three are hoisted. · side by side

Three things to notice. First, the hoisting row is identical — the engine reserves a slot for all three in the memory phase (Lesson 1). Second, the differences split cleanly into two families: var on one side, let/const on the other, with const adding exactly two extra rules on top of let (must initialise, cannot reassign). Third, the last row is the mechanism behind several of the others — let/const live in a private record the engine controls directly (Lesson 3), which is what makes a “locked” state possible at all.

Function Scope vs. Block Scope — And What Loops Do

var is function-scoped: it belongs to the nearest enclosing function (or to Global if there is none), and it ignores every other pair of braces on the way. let and const are block-scoped: they belong to the nearest enclosing { } of any kind — if, for, while, a bare block, a function body.

scope.js
function checkIn() {
  if (true) {
    var floor = 3;            // belongs to checkIn, not to the if
    let wing = "north";       // belongs to the if block only
    const suite = false;      // belongs to the if block only
  }
  console.log(floor);         // 3
  console.log(typeof wing);   // "undefined" — gone with the block
}

Inside checkIn, floor is hoisted to the function's own memory during the memory phase, exactly as if it had been written on the first line of the function — the if braces are invisible to it. wing and suite are created when the block is entered and destroyed when it closes; by line 8 they do not exist anywhere in the scope chain (Lesson 7).

The place this matters most is a for loop, because a loop is a block that runs many times:

loop-var.js
for (var i = 0; i < 3; i++) {
  console.log(i);
}
console.log(i); // 3 — leaked out of the loop
loop-let.js
for (let i = 0; i < 3; i++) {
  console.log(i);
}
console.log(i); // ReferenceError: i is not defined

The visible difference is the last line. The important difference is invisible: with var there is one i for the whole loop, mutated three times. With let, the engine creates a fresh binding for every iteration — three separate slots that happen to share a name — and throws them all away when the loop ends.

step 1 / 6
Memory phase
Before the loop — memory phase

var i is hoisted to the enclosing function (here, Global): one slot, undefined. let i belongs to the loop itself — nothing exists outside the loop yet.

var · one shared slot
1for (var i = 0; i < 3; i++) {
2 console.log(i);
3}
4console.log(i); // 3
memory · 1 slot
function / global · iundefined
(console)
let · one slot per iteration
1for (let i = 0; i < 3; i++) {
2 console.log(i);
3}
4console.log(i); // ReferenceError
memory · 0 slots
no i anywhere
(console)
Step through both loops together. Count the slots: var has one for the whole loop and beyond; let makes a new one for every iteration and throws them all away at the end. · interactive
Why the per-iteration binding matters

Right now the three let slots look like a technicality — the console output is identical. It stops being a technicality the moment something inside the loop body remembers i for later, such as a setTimeout callback. That is the var-prints-3, 3, 3 interview classic, and it is entirely explained by “one slot versus three slots”. Lesson 11 traces it step by step; bank the picture now.

The TDZ, Precisely: Where It Starts And Where It Ends

“Before the declaration” is the informal description. The precise one has two ends:

  • It starts at the top of the enclosing scope — the opening brace of the block (or the first line of the function or file) — not at the line above the declaration. Every line of the scope that comes before the declaration is in the zone.
  • It ends when the declaration is evaluated — when execution actually reaches and runs the let/const line. For let with no initialiser, that moment sets the binding to undefined: unlocked, merely empty, readable without error.
window.js
function checkIn() {
  // TDZ for roomNumber starts here — the top of the scope
  console.log(typeof roomNumber); // ReferenceError
  let roomNumber;                  // ← declaration evaluated: TDZ ends
  console.log(roomNumber);         // undefined — unlocked, just empty
  roomNumber = 101;
}
1·function checkIn() {
2🔒 dead zone // TDZ for roomNumber starts here — the top of the scope
3🔒 dead zone console.log(typeof roomNumber); // ReferenceError
4🔒 dead zone helper(); // ReferenceError if helper reads it
5unlock let roomNumber; // ← declaration evaluated: TDZ ends
6usable console.log(roomNumber); // undefined — unlocked, just empty
7usable roomNumber = 101;
8·}
The TDZ is a stretch of lines, not a point: from the first line of the enclosing scope to the declaration itself. Note that let roomNumber; with no initialiser still ends it — the binding becomes undefined, not locked. · tdz window

Line 4 (let roomNumber;) is the detail people miss. let roomNumber; is not “still uninitialised” — evaluating the declaration is the initialisation, to undefined. After that line, roomNumber behaves like any empty variable. The lock is tied to the declaration being reached, not to a value being provided.

Why “Temporal” — It Is About Time, Not Position

The name is precise. The dead zone is a stretch of time during execution — from the scope being entered until the declaration runs — not a stretch of lines in the file. The cleanest proof is a function that reads the variable, written above the declaration both times, called twice:

temporal.js
function readRoom() {
  return roomNumber; // written ABOVE the let, both times
}

readRoom();          // ① ReferenceError — called before line 6 has run
let roomNumber = 101;
readRoom();          // ② 101 — same function, called after line 6 ran
step 1 / 6
Memory creation phase
Memory phase

readRoom is hoisted in full. roomNumber is hoisted into the TDZ — locked until line 6 is evaluated.

temporal.jsrun A
1function readRoom() {
2 return roomNumber; // written ABOVE the let, both times
3}
4
5readRoom(); // ① called before line 6 runs
6let roomNumber = 101;
7readRoom(); // ② called after line 6 ran
(console)
Global EC · memory
readRoom{ …code }
roomNumber🔒 TDZ
Step through both calls. The read on line 2 sits above the declaration in both runs; only when it runs decides whether it throws. · interactive

Same function, same line 2, same position above the let. Call ① throws, call ② returns 101. Nothing about the code's layout changed between them — only when the read happened relative to the declaration being evaluated. That is the whole meaning of “temporal”, and it is why a lexical (position-based) rule like scope (Lesson 7) and a temporal rule like the TDZ can coexist: scope decides which binding a name refers to; the TDZ decides whether that binding is ready yet.

The TDZ In Disguise: Shadowing And Default Parameters

Two places the TDZ appears where people do not expect a declaration to matter at all.

Shadowing creates a fresh TDZ for the inner name

shadow-tdz.js
let roomNumber = 101;

{
  console.log(roomNumber); // ReferenceError — NOT 101
  let roomNumber = 202;    // this inner let shadows the outer one
}                          // …and its TDZ covers the whole block

The outer roomNumber is fully initialised — 101 is right there. But the block declares its own roomNumber (Lesson 7's shadowing), so inside the block every reference to that name resolves to the inner binding, which is locked from the block's opening brace until line 5. The lookup never climbs to the outer 101, because the nearest match wins — and the nearest match is in its dead zone. Reading line 4, you would swear it prints 101. It throws.

Default parameters are evaluated left to right, in their own scope

param-tdz.js
function book(nights = stay, stay = 1) {
  return nights * stay;
}
book();      // ReferenceError: Cannot access 'stay' before initialization
book(2);     // 2 — nights was given, so `stay` is never read early

Parameters with defaults behave like a sequence of let declarations, evaluated in order. When book() is called with no arguments, the engine evaluates nights = stay first — and stay, one position to the right, is still in its TDZ. Give nights a value and its default expression is never evaluated, so stay is never read early and nothing throws. A default may refer to any parameter to its left, never to its right.

Redeclaration: var Allows It, let And const Don't

redeclare.js
var guestCount = 10;
var guestCount = 20; // totally fine — var allows redeclaring

let roomNumber = 101;
let roomNumber = 102; // SyntaxError: Identifier 'roomNumber' has already been declared

let discountRate = 5;
var discountRate = 10; // also SyntaxError — mixing var with an existing let in the same scope
Combination in the same scopeResult
var declared twiceAllowed
let declared twiceSyntaxError
const declared twiceSyntaxError
var then let/const with the same nameSyntaxError

The Parse-Time Twist: Why Some Errors Crash Everything

Before running a single line, the JS engine reads and checks your entire file first. A duplicate let declaration is caught right here, during this check — which means the error exists before execution has even started.

syntax.js
console.log("This never prints — not even this line!");
let x = 1;
let x = 2; // SyntaxError: Identifier 'x' has already been declared

Compare that with a TDZ violation, which is a genuine runtime error:

tdz.js
console.log("This DOES print completely fine");
console.log(y); // ReferenceError: Cannot access 'y' before initialization
let y = 1;
step 1 / 6
Parse phase · reading the file
Before anything runs — the engine reads both whole files

This is the parse phase. Every line is read and checked; nothing is executed yet. Both files are still just text.

syntax.jsreading…
1console.log("This never prints");
2let x = 1;
3let x = 2; // SyntaxError
(console)
tdz.jsreading…
1console.log("This DOES print");
2console.log(y); // ReferenceError
3let y = 1;
(console)
Step through both files together. One never gets past the read; the other runs normally until the exact line that reads a locked binding. · interactive

Same file structure, wildly different outcomes. A SyntaxError is found while the engine is still reading your file — nothing runs, not even correct code positioned before the mistake. A ReferenceError only happens once execution actually reaches that specific line — everything before it runs completely normally.

The Five Error Types — Full Taxonomy

ErrorCauseExample
ReferenceError: x is not definedName never declared anywhere in scopeconsole.log(neverDeclared)
ReferenceError: Cannot access 'x' before initializationAccessed during the TDZconsole.log(a); let a = 1;
SyntaxError: Identifier has already been declaredRedeclaring a let/constlet x = 1; let x = 2;
SyntaxError: Missing initializer in const declarationAn uninitialized constconst x;
TypeError: Assignment to constant variableReassigning a constconst x = 1; x = 2;
① parse phase · before any line runs
SyntaxError: Identifier has already been declared
let x = 1; let x = 2;
SyntaxError: Missing initializer in const declaration
const x;
② execution phase · at the offending line
ReferenceError: x is not defined
console.log(neverDeclared)
ReferenceError: Cannot access 'x' before initialization
console.log(a); let a = 1;
TypeError: Assignment to constant variable
const x = 1; x = 2;
Five errors, two moments. The top two are found while reading the file, so nothing runs. The bottom three fire only when execution reaches the line. · when each is caught

Five genuinely different mistakes, five genuinely different error names. Interviewers use this table constantly — being able to name the exact error for a given snippet, without running it, is a strong, quick signal that you actually understand what's happening rather than just recognizing keywords.

The typeof Exception: TDZ Breaks The Safety Net

Lesson 6 taught you that typeof is always safe — it never throws, even on a name that was never declared. That rule has exactly one exception, and it's the TDZ:

typeof.js
console.log(typeof unicornVariable); // "undefined" — safe, never declared anywhere

console.log(typeof discountRate);    // ReferenceError! — even typeof can't rescue a TDZ access
let discountRate = 10;
step 1 / 4
Memory creation phase
Memory phase

let discountRate is hoisted: a slot exists, locked (TDZ) until line 4 runs. unicornVariable is declared nowhere — no slot at all.

typeof.js
1console.log(typeof unicornVariable); // "undefined"
2
3console.log(typeof discountRate); // ReferenceError!
4let discountRate = 10;
(console)
Global EC · memory
unicornVariableno slot
discountRate🔒 TDZ
Step through both typeof checks. The difference is not the operator — it is whether a locked slot exists. · interactive

typeof only protects you against a name that doesn't exist at all. It offers zero protection against a name that exists but is still locked in the TDZ — that still throws, safety net or not. This single case is genuinely easy to get wrong, including for experienced developers who rely on the rule “typeof is always safe” without exception.

const: A Frozen Binding, Not A Frozen Value

const is let plus two rules: the declaration must carry an initialiser (otherwise a SyntaxError at parse time — there would never be a moment to give it a value), and the binding can never be reassigned (a TypeError at runtime). Both rules are about the binding — the arrow from the name to the value. Neither says anything about the value at the other end of the arrow.

const.js
const guest = { name: "Aditi", room: 101 };

guest.room = 102;              // fine — the object changed, the binding did not
guest = { name: "Rahul" };     // TypeError: Assignment to constant variable.

const frozen = Object.freeze({ name: "Aditi", room: 101 });
frozen.room = 102;             // silently ignored (throws in strict mode)
console.log(frozen.room);      // 101
const binding
guest
🔒 ──────▶
this arrow cannot be re-pointed
guest = {…} → TypeError
the object · on the heap
  • name"Aditi"
  • room101 → 102 ✓
guest.room = 102 is fine
const freezes the binding — the arrow from the name to the value. The object at the other end of the arrow is as mutable as ever, unless you Object.freeze it. · binding vs value

For primitives (numbers, strings, booleans) the distinction is invisible, because a primitive cannot be changed in place — the only way to “change” it is to re-point the binding, which const forbids. For objects and arrays the distinction is everything: guest.room = 102 reaches through the arrow and edits the object; the arrow itself is untouched. If you want the object frozen too, that is a separate, explicit request — Object.freeze — and it is shallow: nested objects inside remain mutable.

Minimizing Your Own TDZ Window — And Which Keyword To Use

The TDZ exists for every let/const from the top of its scope until its declaration line runs. You can't remove it, but you can make it as short as possible:

  • Declare variables as close as possible to the top of the scope they belong to.
  • Avoid deeply separating a variable's declaration from its first real use.
  • Prefer const by default, use let only when reassignment is genuinely needed, and avoid var entirely in new code — this alone sidesteps almost every mistake in this lesson.
The decision rule, in one line

const unless you will reassign; then let; never var. The reasoning is the matrix above: const gives you block scope, no accidental global property, no silent redeclaration, and a guarantee that the name always points at the same thing — every one of those is a class of bug removed. Reach for let only when the binding genuinely has to move (a loop counter, an accumulator). You will mostly meet var in older code (written before 2015, when let and const arrived) and in interview questions that check you understand why it was replaced.

Real Example — Traced Line By Line

checkin.js
function checkIn() {
  console.log(typeof roomReady); // ReferenceError — TDZ, not "undefined"
  let roomReady = true;
  console.log(roomReady);        // true

  let roomReady = false;         // SyntaxError, caught before checkIn() even runs
}
checkIn();
step 1 / 4
Parse phase
File is parsed first

The engine reads through checkIn's body ahead of time — before checkIn() is ever called.

checkin.js
1function checkIn() {
2 console.log(typeof roomReady);
3 let roomReady = true;
4 console.log(roomReady);
5
6 let roomReady = false; // duplicate
7}
8checkIn();
(console)
checkIn EC · memory
not created yet
Step through the example twice: once as written (rejected before any line runs), then fixed (runs, and hits the typeof TDZ pitfall on its first line). · interactive
  1. File is parsed first

    The engine reads through checkIn's body ahead of time and finds two let roomReady declarations in the same scope — a SyntaxError is raised immediately.

  2. Nothing inside checkIn ever runs

    Not the first console.log, not the assignment, not the second console.log — the whole function body is invalid before checkIn() is ever called.

  3. Fixing it

    Remove the duplicate declaration, and now the first line correctly throws a different error — a TDZ ReferenceError from the typeof pitfall — since roomReady hasn't been declared yet at that point.

See It Live — Console & DevTools

  1. Open the console

    Any page → DevTools (F12) → Console tab.

  2. Trigger a SyntaxError

    console
    let a = 1;
    let a = 2; // SyntaxError — paste both lines together as one snippet
  3. Trigger a TDZ ReferenceError

    console
    console.log(typeof b); // ReferenceError
    let b = 1;
  4. Compare with an undeclared name

    console
    console.log(typeof totallyUnknown); // "undefined" — no error at all
  5. Watch the TDZ in the Scope panel

    scope-panel.js
    function checkIn() {
      debugger;            // paused here, roomNumber is in the TDZ
      let roomNumber = 101;
      debugger;            // paused here, it is 101
    }
    checkIn();

    At the first pause, open Scope → Local: Chrome lists roomNumber already, with the value <value unavailable> — its way of showing a binding that exists but is locked. Resume to the second pause and the same entry reads 101. That is the TDZ, rendered by the engine itself.

  6. Count the loop slots

    console
    for (let i = 0; i < 3; i++) { debugger; }

    Each time it pauses, Scope → Block shows an i — and stepping to the next iteration shows a new Block scope, not the same one with a bigger number. Swap let for var and the Block scope disappears: i now sits in Local (or Global) for the whole loop.

  7. Confirm the difference side by side

    console
    console.log("hello");
    let a = 1;
    let a = 2;

    Paste this as one snippet. Only the SyntaxError appears and hello never prints. That is proof the error was caught before any line ran. Now remove the last line and paste again: hello prints.

Common Misconceptions

“All JavaScript errors behave the same way — they just stop at the line that's wrong.” Not true. A SyntaxError is caught while the engine is still reading your file, before anything runs — even correct code above the mistake never executes. A ReferenceError or TypeError only fires once execution actually reaches that line.

“typeof never throws, under any circumstances.” It doesn't throw for a name that was never declared — but it does throw for a name that exists and is still stuck in the TDZ.

“var and let behave the same way regarding redeclaration, just with different scoping.” They don't — var happily allows redeclaring the same name in the same scope; let/const throw a SyntaxError immediately if you try.

“The TDZ is the lines above the declaration.” It is the time before the declaration is evaluated. A function written above the let reads it fine when called after the let has run, and throws when called before — same lines, different moment.

“If the outer variable is initialised, an inner one with the same name is safe to read.” Shadowing gives the inner name its own binding with its own TDZ, and the lookup stops at the nearest match — so the initialised outer value is never even consulted.

“const makes the object immutable.” It makes the binding immutable. Properties of the object can still be changed; only re-pointing the name is forbidden. Freezing the value is a separate operation.

Interview Questions

Is let hoisted? Then why does reading it early throw instead of giving undefined?

Yes, all three keywords are hoisted — a slot is reserved in the memory phase. The difference is the slot's starting state: var is initialised to undefined immediately; let/const are left uninitialised (locked) until their declaration is evaluated. Reading a locked binding throws a ReferenceError. The locked stretch is the Temporal Dead Zone.

What is the scope of var versus let/const?

var is function-scoped: it belongs to the nearest enclosing function (or Global) and ignores all other braces. let/const are block-scoped: they belong to the nearest enclosing { } of any kind and cease to exist when it closes.

What does for (let i = 0; …) do differently from for (var i = 0; …)?

With var there is one i slot for the whole loop, which survives after it ends. With let the engine creates a fresh binding for every iteration — three iterations, three separate slots — and discards them when the loop ends, so i is not defined afterwards. The per-iteration binding is what makes closures inside the loop capture the right value.

Exactly where does the TDZ start and end?

It starts at the top of the enclosing scope (the opening brace, or the first line of the function/file), not at the line just above the declaration. It ends the moment the declaration is evaluated — for let x; with no initialiser, that sets x to undefined and unlocks it.

Why is it called the *Temporal* Dead Zone?

Because it is defined by time, not position. A function that reads a let variable can be written above the declaration and still work — as long as it is called after the declaration has been evaluated. The same function called before that moment throws. Scope is decided by where code is written (lexical); readiness is decided by when it runs (temporal).

Can shadowing cause a TDZ error even when the outer variable is initialised?

Yes. An inner let with the same name creates a separate binding with its own TDZ covering the whole inner block. Lookups inside the block resolve to the inner binding first (nearest match wins), so reading the name before the inner declaration throws — the initialised outer value is never reached.

Why does function f(a = b, b = 1) {} throw when called with no arguments?

Default parameters are evaluated left to right in their own scope, like a sequence of let declarations. Evaluating a = b reads b while it is still in its TDZ. A default may refer to parameters on its left, never on its right.

Does const make a value immutable?

No — it makes the binding immutable: the name can never be re-pointed, and it must be given a value on the declaration line. An object or array it points to can still be mutated through it. Object.freeze is the (shallow) way to freeze the value itself.

Can you redeclare a var in the same scope? Can you redeclare a let?

var can be redeclared freely in the same scope. let/const throw a SyntaxError if redeclared in the same scope.

Why does a SyntaxError sometimes prevent even correct code from running?

Because the engine parses the entire file before executing any of it — a SyntaxError is caught during that parse phase, so nothing in the file runs at all, not even code positioned before the mistake.

Name the five error types covered here, and what triggers each.

“is not defined” (never declared), “cannot access before initialization” (TDZ), duplicate declaration (redeclaring let/const), missing initializer (uninitialized const), and assignment to constant (reassigning const).

Does typeof ever throw an error?

Yes — one exception. It's safe on a name that was never declared at all, but it still throws if the name exists and is currently inside the TDZ.

How can you minimize the practical impact of the TDZ in your own code?

Declare variables as close to the top of their scope as possible, and default to const, using let only where reassignment is truly needed.

What's the difference between a SyntaxError and a runtime error like ReferenceError or TypeError?

A SyntaxError is found while the engine reads the file, before execution begins. ReferenceError and TypeError only happen once execution reaches the offending line.

Published by Jainik · Last updated · How these lessons are made

Keep learning

Part of the study guide

Related series