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;
}
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);
}
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.
More JavaScript & runtime errors
Browse the full reference for JavaScript, Node.js, and database errors — exact message, cause, and fix.