What Is the Temporal Dead Zone (TDZ) in JavaScript? How to Fix 'Cannot Access Before Initialization'
Short answer: the Temporal Dead Zone (TDZ) is the stretch of code between
the start of a scope and the line where a let, const or class is
initialized. The variable exists in that scope, but touching it throws a
ReferenceError. Fix it by moving the declaration above the first use, or by
making sure nothing runs before the declaration does.
console.log(price); // ReferenceError
const price = 10; Last reviewed October 2026.
The error message, by browser
The same TDZ error is worded differently depending on the engine, which is why searching for the exact text you see can lead to different pages:
| Engine | Error message |
|---|---|
| Chrome, Edge, Node.js (V8) | ReferenceError: Cannot access 'x' before initialization |
| Firefox | ReferenceError: can't access lexical declaration 'x' before initialization |
| Safari | ReferenceError: Cannot access uninitialized variable. |
All three mean the same thing. If you see x is not defined instead, that is a
different problem (see below).
Table of Contents
- Understanding the Problem
- The Science Behind TDZ
- Real-World Examples and Solutions
- Advanced TDZ Scenarios
- Debugging and Prevention Strategies
- Best Practices and Tooling
- ReferenceError: Temporal is not defined
Understanding the Problem
Have you ever hit "Cannot access before initialization" while refactoring
JavaScript that used to work? Or wondered why your let and const variables
behave differently from var? That is the Temporal Dead Zone (TDZ) at work,
one of JavaScript's most misunderstood behaviors.
Not every ReferenceError is a TDZ error
Before debugging, check which error you actually have:
Cannot access 'x' before initialization: TDZ. The variable exists in the scope, but you used it before its declaration ran.x is not defined: the variable is not in scope at all, or was never declared. This is a scoping problem, not TDZ.Assignment to constant variable: aTypeError. You reassigned aconst.
A common source of confusion is converting var to let and finding that code
breaks. That is usually a scoping difference, not TDZ:
// Original code with var
function processUser(user) {
if (user.isActive) {
var status = "Processing user...";
}
return status; // "Processing user..." or undefined
}
// Refactored to let: breaks, but this is NOT TDZ
function processUser(user) {
if (user.isActive) {
let status = "Processing user...";
}
return status; // ReferenceError: status is not defined
} var is function-scoped, so status leaked out of the if block. let is
block-scoped, so status doesn't exist outside it. The fix is to declare
let status; before the if. TDZ is a separate rule about timing within a
scope where the variable does exist.
The Science Behind TDZ
What Exactly Is the Temporal Dead Zone?
The Temporal Dead Zone is the time period between entering a scope and the
actual declaration/initialization of a let, const or class binding.
During this period, the variable exists in the scope but is "uninitialized", and
any attempt to read or write it throws a ReferenceError.
The Variable Lifecycle: A Deep Dive
Every JavaScript variable goes through distinct phases:
// Phase 1: Creation (the binding is created when the scope is entered)
// Phase 2: Initialization
// Phase 3: Assignment
// VAR - creation and initialization (to undefined) both happen at scope entry
function varExample() {
console.log(x); // undefined
var x = 5;
console.log(x); // 5
}
// LET - created at scope entry, initialized when the declaration runs
function letExample() {
console.log(y); // ReferenceError: Cannot access 'y' before initialization
let y = 5;
console.log(y); // 5
}
// CONST - same timing as let, and must be assigned at declaration
function constExample() {
console.log(z); // ReferenceError: Cannot access 'z' before initialization
const z = 5;
console.log(z); // 5
} Memory and Scope: Under the Hood
When JavaScript enters a new scope, here's what happens:
- Scanning phase: the engine finds all declarations in the scope
- Environment setup: it creates a binding for each variable
varbindings are immediately initialized toundefinedlet,constandclassbindings stay uninitialized (the TDZ) until their declaration line executes
function demonstrateTDZ() {
// A variable that was never declared: typeof is safe
console.log(typeof neverDeclared); // "undefined"
// A variable in its TDZ: typeof throws
console.log(typeof tdzVar); // ReferenceError!
let tdzVar = "now initialized";
} This is why typeof is no longer a "safe" check once you use let or const:
it only protects you from variables that don't exist, not from ones that exist
but aren't initialized yet.
Real-World Examples and Solutions
1. Shadowing: An Inner Declaration Hides the Outer Variable
Problem: a let or const inside a block applies to the whole block, even
the lines above it.
let count = 1;
{
console.log(count); // ReferenceError: Cannot access 'count' before initialization
let count = 2; // This inner count shadows the outer one for the entire block
} You might expect the first console.log to print the outer 1. It doesn't: the
inner count exists from the start of the block, so the outer one is hidden and
the inner one is still uninitialized.
Solution: rename the inner variable, or move its declaration above its first use.
let count = 1;
{
const innerCount = 2;
console.log(count, innerCount); // 1 2
} 2. Calling a Function Before a const It Uses Is Initialized
Problem: this is the most common real-world TDZ bug. A function reads a
const that hasn't been initialized yet at the moment it is called.
function getPort() {
return config.port; // fine to write, but...
}
console.log(getPort()); // ReferenceError: Cannot access 'config' before initialization
const config = { port: 3000 }; Function declarations are hoisted, so getPort can be called early. But
config is still in its TDZ when the call happens.
Solution: initialize first, or call later.
const config = { port: 3000 };
function getPort() {
return config.port;
}
console.log(getPort()); // 3000 3. Function Hoisting vs Variable Hoisting
Problem: mixing function and variable declarations.
function confusingExample() {
console.log(typeof myFunc); // "function" - function declarations are fully hoisted
console.log(typeof myVar); // ReferenceError - let variables are in TDZ
function myFunc() {
return "I'm hoisted completely!";
}
let myVar = "I'm in TDZ until here!";
} Solution: understand the difference, and declare variables before you use them.
function clearExample() {
let myVar = "Safe to use from here";
console.log(myFunc()); // Works: declarations are hoisted
console.log(myVar);
function myFunc() {
return "I'm hoisted completely!";
}
} 4. Classes Have a TDZ Too
Problem: class declarations are not usable before the line that defines
them.
const user = new User("Ada"); // ReferenceError: Cannot access 'User' before initialization
class User {
constructor(name) {
this.name = name;
}
} Solution: define the class above its first use.
class User {
constructor(name) {
this.name = name;
}
}
const user = new User("Ada"); A related point: class fields are initialized in order, so reading a field that
is defined later gives undefined, not a TDZ error.
class Profile {
name = this.getName(); // runs first
type = "user"; // initialized after name
getName() {
return `${this.type}_profile`; // this.type is undefined here: "undefined_profile"
}
} If that matters, define type before name, or set both in the constructor.
Advanced TDZ Scenarios
1. TDZ in Module Scope
// module.js
console.log(moduleVar); // ReferenceError: Cannot access 'moduleVar' before initialization
export const moduleVar = "I'm a module variable"; With circular imports the same thing can happen across files: module A imports
module B, which imports A and reads one of A's const exports before A has
finished evaluating. The fix is to break the cycle, usually by moving the shared
value to a third module.
2. TDZ with Destructuring
// Destructuring declarations are in the TDZ until they run
function destructuringTDZ() {
console.log(a); // ReferenceError
let { a, b } = { a: 1, b: 2 };
}
// Default parameters are evaluated left to right
function defaultParamTDZ(x = y, y = 1) {
return x + y; // ReferenceError: Cannot access 'y' before initialization
}
// Solution: reorder parameters
function fixedDefaultParam(y = 1, x = y) {
return x + y; // Works fine
} 3. TDZ in Switch Statements
function switchTDZ(value) {
switch (value) {
case "a":
let result = "Case A"; // The whole switch body is one scope
break;
case "b":
console.log(result); // ReferenceError! 'result' is declared but not initialized
break;
}
}
// Solution: give each case its own block
function fixedSwitch(value) {
switch (value) {
case "a": {
let result = "Case A";
console.log(result);
break;
}
case "b": {
let result = "Case B";
console.log(result);
break;
}
}
} Debugging and Prevention Strategies
1. Visual TDZ Debugging
Think of the TDZ as a window that opens at the top of the scope and closes at the declaration:
function visualizeTDZ() {
// ┌─ TDZ START for 'data' ─┐
console.log("Before declaration"); // fine: doesn't touch data
if (Math.random() > 0.5) {
console.log(data); // ← ReferenceError here
}
let data = "initialized"; // ← TDZ END
// └───────────────────────┘
console.log(data); // Safe to use
} 2. Detecting TDZ Errors in Code
// Check whether an error is a TDZ error (covers V8, Firefox and Safari wording)
function isTDZError(error) {
return (
error instanceof ReferenceError &&
(error.message.includes("before initialization") ||
error.message.includes("uninitialized variable"))
);
}
function testTDZ() {
try {
console.log(myVar);
} catch (error) {
console.log(isTDZError(error)); // true
}
let myVar = "now initialized";
} This is mostly useful in tests and error-reporting code, not as a way to "handle" TDZ in production. The real fix is to change the order of the code.
3. Progressive Enhancement Strategy
// Strategy: migrate from var to let/const gradually
// Step 1: Identify var usage
function oldCode() {
var x = 1;
if (true) {
var y = 2;
}
return x + y;
}
// Step 2: Add linting rules (below) to catch problems early
// Step 3: Migrate in small steps, with tests
function newCode() {
let x = 1;
let y; // Declare at the scope where it's needed
if (true) {
y = 2;
}
return x + y;
} Best Practices and Tooling
1. ESLint Rules for TDZ Prevention
no-use-before-define catches many direct use-before-declaration cases, and
no-var removes the var hoisting behavior that hides them. In ESLint's flat
config:
// eslint.config.js
export default [
{
rules: {
"no-use-before-define": [
"error",
{ functions: false, classes: true, variables: true },
],
"no-var": "error",
},
},
]; Lint rules can't catch everything. The "function called before the const it
reads is initialized" case (example 2 above) depends on call order at runtime.
2. TypeScript Catches Many TDZ Errors at Compile Time
TypeScript reports direct use-before-declaration errors without running the code,
for example Block-scoped variable 'x' used before its declaration for let and
const, and Class 'X' used before its declaration for classes. It does not
catch every runtime ordering problem, so tests still matter.
3. Code Review Checklist
- Are all
let/const/classdeclarations above their first use? - Does any function that reads a
constget called before thatconstis initialized? - Does an inner block redeclare a name from an outer scope (shadowing)?
- Do
switchcases that declare variables use their own blocks? - Are there
vartolet/constconversions that need scope adjustment?
4. Safe Refactoring Patterns
// Pattern 1: Declaration grouping
function safeDeclarations() {
// Group all declarations at the top
let userId, userType, userData;
// Then perform assignments
if (someCondition) {
userId = getCurrentUserId();
userType = determineUserType(userId);
userData = fetchUserData(userId, userType);
}
return { userId, userType, userData };
}
// Pattern 2: Early return pattern
function earlyReturn(config) {
// Handle edge cases early
if (!config) return null;
if (!config.isValid) return null;
// Now safe to declare and use variables
const processedData = processConfig(config);
const result = validateData(processedData);
return result;
}
// Pattern 3: Factory function pattern
function createProcessor(options) {
const processor = {
data: null,
config: options,
initialize() {
this.data = this.processConfig();
return this;
},
processConfig() {
return { processed: true, config: this.config };
},
};
return processor.initialize();
} 5. Testing TDZ Scenarios
describe("TDZ handling", () => {
test("using a const before its declaration throws a ReferenceError", () => {
function broken() {
value; // eslint-disable-line no-unused-expressions
const value = 1;
}
expect(() => broken()).toThrow(ReferenceError);
});
test("declaring the variable first avoids the error", () => {
function fixed(condition) {
let result;
if (condition) {
result = "success";
}
return result || "default";
}
expect(fixed(true)).toBe("success");
expect(fixed(false)).toBe("default");
});
}); ReferenceError: Temporal is not defined
This is not a temporal dead zone error, even though the word matches.
Temporal is the newer JavaScript date and time API. The error means the browser
or runtime you're using doesn't provide it, so Temporal isn't a defined global
there.
Check current browser and Node.js support (MDN's compatibility table is the
fastest way), or use a polyfill such as @js-temporal/polyfill while support is
uneven.
Conclusion: From Confusion to Mastery
The Temporal Dead Zone isn't just a JavaScript quirk. It is part of how modern
JavaScript's block scoping makes code more predictable. Once you know the rule
(a let, const or class can't be used before its declaration runs, even if
it is hoisted), most of these errors take seconds to fix.
Key Takeaways
- TDZ is a feature, not a bug. It prevents access to uninitialized variables.
- Not every ReferenceError is TDZ.
x is not definedis a scope problem. typeofis not safe in the TDZ.- Tools help, but order matters. Linting and TypeScript catch many cases; runtime call order catches the rest.
Your Next Steps
- Search your code for functions that read a
constdefined further down the file. - Turn on
no-use-before-defineandno-varin your lint config. - Run the examples above in your browser console to see each error message.
Inheriting a messy or AI-generated JavaScript codebase? I audit and stabilize them. Book a technical diagnostic.
Resources and Further Reading
- MDN: let, the temporal dead zone
- MDN: ReferenceError: can't access lexical declaration before initialization
- ESLint: no-use-before-define
- Related on this site: Understanding closures in JavaScript, Variable naming best practices
Found this guide helpful? Consider supporting the author:
Frequently asked questions
What is the temporal dead zone (TDZ) in JavaScript?
The temporal dead zone is the period between entering a scope and the line where a let, const or class binding is initialized. The binding already exists in that scope, but reading or writing it throws a ReferenceError until execution reaches its declaration.
Is const hoisted in JavaScript?
Yes. The binding is created when the scope is entered, which is why a const in an inner scope shadows an outer variable of the same name even before its declaration line. Unlike var, it is not initialized to undefined, so using it early throws a ReferenceError instead of returning undefined.
Does typeof protect you from the temporal dead zone?
No. typeof returns 'undefined' for a variable that was never declared, but it throws a ReferenceError for a let or const variable that is still in its temporal dead zone.
What does 'ReferenceError: Temporal is not defined' mean?
It is not a temporal dead zone error. Temporal is the newer JavaScript date and time API, and the error means the browser or runtime you are using does not provide it. Check current support or use a Temporal polyfill.
Debug AI features without guessing
A production-focused sequence for separating model failure from application failure across prompts, APIs, and workflows.
If you found this article helpful, consider buying me a coffee to support more content like this.
Related Articles

Learn the key differences between debouncing and throttling in JavaScript, how to implement them, and when to use each technique for optimal performance. Discover real-world examples and best practices for handling frequent events in web applications.

Learn the best practices for naming variables in JavaScript to enhance code readability and maintainability. This comprehensive guide covers the importance of good variable names, practical tips for naming variables, and examples to illustrate the impact of clear and descriptive variable naming. Elevate your JavaScript coding skills by mastering the art of choosing meaningful and consistent variable names.

Discover how the safe assignment operator (`?=`) simplifies error handling in JavaScript by eliminating the need for verbose `try/catch` blocks, making your code more readable and maintainable.
Debug AI features without guessing
A production-focused sequence for separating model failure from application failure across prompts, APIs, and workflows.