Quick answer
You read or wrote a let/const/class binding before its declaration line actually ran — the temporal dead zone (TDZ). Unlike var, which hoists as undefined, let/const hoist the name but leave it uninitialized and throw on any access. Fix it by moving the declaration above the use, not by switching back to var.
The exact error string
console.log(count);
let count = 10;
// Uncaught ReferenceError: Cannot access 'count' before initialization
{
console.log(x); // ❌ same TDZ, even though x exists later in this block
let x = 5;
}
What it actually looks like
ReferenceError even though the name does exist — that is precisely the temporal dead zone. Compare x is not defined, where the name genuinely resolves nowhere; a var in the same position would have given you undefined instead.Why let/const behave differently from var
var declarations are hoisted and initialized to undefined immediately, so an early read just silently gives undefined — a quieter, harder-to-spot bug. let/const/class are hoisted but stay uninitialized from the top of the scope until their declaration line runs. That span is the temporal dead zone: any access inside it throws, on purpose, so the mistake surfaces immediately instead of silently producing undefined.
console.log(a); // undefined — var hoists AND initializes
var a = 1;
console.log(b); // ❌ Cannot access 'b' before initialization — TDZ
let b = 1;
Cause 1: using a variable before its declaration line
function greet() {
console.log(message); // ❌ still in message's TDZ
let message = "hi";
}
function greet() {
let message = "hi"; // ✅ declare first
console.log(message);
}
Cause 2: a self-referencing default parameter
function f(a = a) { }
f();
// ❌ Cannot access 'a' before initialization — a's default reads a itself
function f(a, b = a) { } // ✅ default refers to a different, already-set param
f(5);
Cause 3: class fields that depend on each other
class Config {
timeout = retries * 1000; // ❌ retries hasn't initialized yet (declared below)
retries = 3;
}
class Config {
retries = 3; // ✅ declare first
timeout = this.retries * 1000;
}
Cause 4: block scope shadowing an outer variable
let value = "outer";
{
console.log(value); // ❌ NOT "outer" — this block's own `value` shadows it,
let value = "inner"; // and that inner `value` is in its TDZ right here
}
let value = "outer";
{
let value = "inner"; // ✅ declare before any reference inside the block
console.log(value);
}
The typeof escape hatch does not work here
There is a long-standing JavaScript idiom for probing whether a name exists: typeof x returns the string "undefined" for a completely undeclared identifier instead of throwing. It is the one operator that is safe on a name that does not exist. That guarantee does not extend to the temporal dead zone:
"use strict";
console.log(typeof neverDeclared); // ✅ "undefined" — safe, no error
console.log(typeof later); // ❌ ReferenceError: Cannot access 'later' before initialization
let later = 1;
Verified on Node v24.15.0. This matters because typeof guards are exactly what people reach for when a value might not be ready yet — and here the guard itself is what throws. If you need a "has this been set up?" check, use a separate flag or a nullish check after the declaration; there is no expression that can safely inspect a binding while it is in the TDZ.
class declarations are in the TDZ too
The TDZ is usually taught as a let/const rule, which leaves out a case that produces the same message from code that contains neither keyword:
new Thing(); // ❌ ReferenceError: Cannot access 'Thing' before initialization
class Thing {}
greet(); // ✅ works — function declarations ARE hoisted and initialized
function greet() { console.log("hi"); }
Classes are hoisted exactly like let: the binding exists from the top of the scope but stays uninitialized until the declaration is evaluated. Function declarations are the genuine exception — they are fully hoisted, which is why the two lines above behave differently despite looking parallel. If you are converting a codebase from function declarations to classes, this is the failure you will meet first, and it usually means a module is importing something in a circular loop or a file is being ordered by a bundler differently than you assumed.
Common variants at a glance
| Pattern | Why it's TDZ | Fix |
|---|---|---|
| use before declare, same scope | declaration line hasn't executed yet | move the declaration up |
function f(a = a) | default value reads its own uninitialized param | reference a different param, or restructure |
| class field ordering | a field depends on one declared below it | reorder fields top-to-bottom by dependency |
| shadowed block variable | inner let shadows outer name for the whole block | declare the inner variable first, or rename it |
switching to var "fixes" it | removes the check, not the bug | reorder code instead |
Debugging checklist
- ✓ Find every use of the named variable in its enclosing scope — is one before the
let/constline? - ✓ Check for a same-named variable declared later in the same block that shadows an outer one
- ✓ Default parameters: does
a = a(or a chain) reference itself before assignment? - ✓ Class fields: does one field's initializer use another field declared below it?
- ✓ Fix by reordering declarations before use — don't switch to
varto silence it
Frequently Asked Questions
What is the temporal dead zone (TDZ)?
The span of code between the start of a scope and the line where a let/const/class binding is actually declared. The name is hoisted (JavaScript knows it exists) but is uninitialized during that span — any read or write attempt throws "Cannot access before initialization" instead of silently returning undefined.
Why doesn't this happen with var?
var declarations are hoisted AND initialized to undefined immediately, so reading one before its assignment line just gives undefined with no error — a different, quieter bug. let/const/class are hoisted but left uninitialized until their declaration executes, which is exactly what makes the TDZ detectable and why this error exists.
Why does a self-referencing default parameter throw this?
function f(a = a) { } tries to read a while evaluating a's own default value, before a has been assigned — a is in its own TDZ at that point. Reference a different, already-initialized variable for the default, or restructure so the parameter doesn't depend on itself.
Why does this happen inside a class field or constructor?
Class declarations are also subject to the TDZ — referencing a class (or one of its static/instance fields that depends on another not-yet-initialized field) before the class body finishes evaluating throws the same error. Order class fields so each only depends on fields declared above it.
Why does swapping let for var 'fix' this?
It removes the error by removing the safety net, not by fixing the underlying issue — the variable is still logically used before it has a meaningful value, now silently as undefined. Prefer reordering your code so the declaration runs before any use, rather than switching to var to suppress the error.
How do I fix 'Cannot access before initialization' quickly?
Move the variable's declaration above its first use, or move the use below the declaration. If a block-scoped loop or closure is involved, confirm you're not capturing a reference to a binding that hasn't run yet in that particular iteration or call.
References
More JavaScript & runtime errors
Browse the full reference for JavaScript, Node.js, and database errors — exact message, cause, and fix.