Skip to main content

What is temporal dead zone (TDZ) in JavaScript

Temporal Dead Zone (TDZ) is the period from the start of a block scope to the line where let or const is initialized. Accessing the variable during this period throws a ReferenceError.

Theory

TL;DR

  • TDZ is like a shelf marked "reserved": the slot exists in memory, but reading it throws an error.
  • var initializes to undefined on hoist. let/const stay uninitialized until the assignment line.
  • Accessing let/const before that line gives ReferenceError, not undefined.
  • Use let/const in all modern code. TDZ turns silent bugs into explicit errors.

Quick example

javascript
console.log(a); // ReferenceError: Cannot access 'a' before initialization let a = 5; console.log(a); // 5 console.log(b); // undefined - no TDZ for var var b = 5;

Both variables are hoisted. But var b gets undefined immediately, while let a stays uninitialized until line 2 runs.

Key difference

var hoists and auto-initializes to undefined, so a pre-declaration read returns a value without error. let and const hoist only the binding into the lexical environment, leaving it uninitialized. Any read before the assignment line triggers a ReferenceError. The TDZ ends the moment the assignment executes.

When to use

  • Block-scoped variable that changes value → let.
  • Value that should stay fixed → const (TDZ plus no reassignment).
  • Old for loop in a pre-ES6 library → var, no other reason.
  • React hooks, Express handlers, module imports → const by default.

Comparison table

VariableHoistedInitialized on hoistTDZPre-init access
varYesundefinedNoReturns undefined
letYesNoYesReferenceError
constYesNoYesReferenceError

Classes declared with class follow the same rule as let. Accessing the class before its declaration line throws.

How the engine handles this

V8 runs two passes. The first hoists var declarations and sets them to undefined. For let/const, it creates an uninitialized binding in the lexical environment record. The second pass executes line by line. A read on an uninitialized binding throws ReferenceError. That is all TDZ is at the engine level.

Common mistakes

Assuming let is not hoisted at all:

javascript
let x; console.log(x); // undefined - declared but not yet assigned x = 10; console.log(x); // 10

let is hoisted. It just stays uninitialized. That is why accessing it before the declaration line gives "cannot access before initialization", not "x is not defined".

Destructuring params without defaults:

javascript
function fn({ a }) { console.log(a); } fn(); // ReferenceError - destructured binding has its own TDZ fn({}); // undefined - fine

Fix: function fn({ a = 1 }).

Confusing ReferenceError with TypeError:

javascript
const x = 1; x = 2; // TypeError, not ReferenceError

ReferenceError happens during TDZ, before init. TypeError happens after TDZ ends, when you try to reassign a const. Different errors, different causes.

Most TDZ bugs I've seen in code review come from refactoring var to let without checking whether the variable is read before its declaration in the same block.

Real-world usage

  • React: const [state, setState] = useState(null) - TDZ prevents reads before the hook line.
  • Express: const id = req.params.id inside a route handler - catches accidental pre-parse access.
  • Node.js modules: const fs = require('fs') at top level - strict mode enforces TDZ throughout.
  • Redux: const action = { type: 'FETCH_USER' } - prevents undefined action types from reaching reducers.

Follow-up questions

Q: What is the output of console.log(x); let x = 5;?
A: ReferenceError: Cannot access 'x' before initialization. The variable is hoisted but stays in TDZ until the assignment runs.

Q: Does const have a TDZ?
A: Yes, same rules as let. TDZ ends at the assignment line. After that, trying to reassign throws TypeError, not ReferenceError.

Q: Why does TDZ exist?
A: It makes early variable access a loud, explicit error instead of returning undefined without error. With var, pre-declaration reads return undefined with no warning, which can cause bugs that are hard to trace.

Q: How does Babel simulate TDZ when transpiling to ES5?
A: Babel uses a void 0 sentinel check to approximate the uninitialized state. It gets close to the spec behavior, but edge cases can differ from native engine handling.

Examples

Basic TDZ behavior

javascript
// Both variables are hoisted from the start of the scope. // var: initialized to undefined immediately. // let: stays uninitialized until the declaration line runs. console.log(name); // undefined (var - no TDZ) var name = 'Alice'; console.log(age); // ReferenceError: Cannot access 'age' before initialization let age = 30;

name returns undefined because var initializes on hoist. age throws because let leaves its binding uninitialized until line 9.

TDZ in a request handler

javascript
// Safe pattern - access happens after declaration function handleLogin(userId) { if (!userId) throw new Error('Missing user ID'); const token = generateToken(userId); // TDZ has passed by this line return token; } // Unsafe pattern - TDZ violation function badHandler(userId) { console.log(token); // ReferenceError - token is in TDZ here const token = generateToken(userId); return token; }

In handleLogin, token is only read after its declaration. In badHandler, reading token before the const line throws immediately. TDZ surfaces this in development, not in production.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?