skip to content

A JavaScript app throws `ReferenceError: Cannot access 'config' before initialization` at startup. What does that specific message tell you, and how do you track down the cause?

level: seniorimportance: should knowfreq 38%

answer

  1. two ReferenceErrors, two meanings
  2. binding found, value missing
  3. who runs before that line?
  4. written below, invoked above
  5. reorder or defer, never guard

basics

~20 s

That message means the binding exists in scope but is still uninitialized: something declared with let, const, or class was accessed before its declaration ran. Find what executes during the scope's setup and reaches the name early, rather than looking for a missing declaration.

solid answer

~50 s

The wording is diagnostic. `'config' is not defined` would mean no binding exists anywhere in scope; `Cannot access 'config' before initialization` means the binding *does* exist and is a `let`, `const`, or `class` whose declaration has not been evaluated yet. So stop looking for a typo or a missing import-like declaration and start looking at execution order. Because the temporal dead zone is about time rather than source position, the throwing code is often written *below* the declaration but invoked above it — typically a helper or factory called during the scope's own setup, or a class instantiated before its `class` statement. Read the stack from the bottom up to find the first frame in the declaring scope, and check what ran before the declaration line. Fixes are ordering fixes: move the invocation after the declaration, defer the access into a function called later, or hoist the work into a function declaration.

code

javascript · 13 lines
javascript
function createClient() {
  return { url: config.url }; // written below the declaration...
}

try {
  const client = createClient(); // ...but invoked above it
} catch (e) {
  console.log(e.message); // Cannot access 'config' before initialization
}

const config = { url: '/api' };

console.log(createClient()); // { url: '/api' } - same call, after initialization

go deeper

for a junior

Recognise the message: the variable exists but its let, const, or class declaration has not run yet. Say that something used the name too early rather than that the declaration is missing.

for a middle

Explain how it differs from "is not defined" and identify the usual shapes — an initialization call at the top of the scope, or a new above its class declaration — plus the fix of moving or deferring the access.

for a senior

Walk a real diagnosis: read the stack bottom-up to the frame in the declaring scope, work out what executed before the declaration line, check for a block-level shadow, and confirm by relocating the declaration.

for a principal

Defend the crash as the desired outcome: reverting to var would restore a silent undefined that corrupts state downstream, so the standard should be fail-fast bindings plus an explicit initialization order rather than incidental ordering.

## Read the message first JavaScript gives you two very different `ReferenceError`s and they point at different bugs. - **`config is not defined`** — the name resolves to nothing in the whole scope chain. Suspect a typo, a declaration that was deleted, or code running in a scope that never saw it. - **`Cannot access 'config' before initialization`** — the name resolved. There *is* a binding for it in the current or an enclosing scope, declared with `let`, `const`, or `class`, and it is still uninitialized. This is purely an ordering bug. (Engines word the second one differently — SpiderMonkey says "can't access lexical declaration 'config' before initialization", JavaScriptCore says "Cannot access uninitialized variable" — but all mean the same state.) Confusing the two wastes the most time in this bug, because the instinct on any `ReferenceError` is to go hunting for a missing declaration, and here the declaration is right there in the file. ## Why the throw is usually far from the declaration The dead zone is temporal. Code written above a `const` fails; code written below it also fails if it *runs* first. The second case is the one that reaches production: ```js function createClient() { return { url: config.url }; // written below, runs above } const client = createClient(); // throws const config = { url: '/api' }; ``` `createClient` looks correct in isolation and works everywhere else in the program. It throws only on this one call, made while `config` is still uninitialized. Common shapes of the same mistake: - A top-of-scope initialization call — `setup()`, `register()`, `Object.freeze(buildDefaults())` — that transitively reads a `const` declared lower down. - A callback registered and *synchronously invoked* during setup (an event emitter that replays, a plugin that calls its hook immediately) whose body closes over a later `const`. - `new Foo()` above `class Foo {}`. Class declarations sit in the dead zone exactly like `const`, and this one surprises people who remember that function declarations are fully available before their line. - A block-level shadow: an inner `const config` further down a block makes every earlier reference in that block resolve to the inner, uninitialized binding, even though an outer `config` exists and is fine. ## The diagnostic procedure 1. **Take the name from the message** and find its lexical declaration. Confirm it is `let`, `const`, or `class` — it always is. 2. **Read the stack bottom-up** to find the first frame that belongs to the scope containing that declaration. That frame's line number tells you *when* the scope was interrupted, and it is almost always above the declaration. 3. **Ask what ran before the declaration line** in that scope. Any call expression, any side-effecting initializer, any immediately invoked function is a candidate. 4. **Check for shadowing** if the outer name looks initialized: search the enclosing block for a second declaration of the same name below the failing line. 5. **Reproduce by moving the declaration** to the very top of its scope. If the error disappears, the diagnosis is confirmed and you have already found one valid fix. ## Fixing it The fix is always to change *when* things happen, never to add a defensive check — you cannot guard an uninitialized binding, because `typeof config` and `config !== undefined` both throw as hard as the original access. - **Reorder.** Move the declaration above the code that needs it. Cheapest fix, correct when there is no cycle in the dependencies. - **Defer the access.** Wrap the work in a function and call it after setup completes, so the read happens once the binding is initialized. Turning an eager `const client = createClient()` into a lazily called `getClient()` is the standard move. - **Use a function declaration for the entry point.** Function declarations are initialized during scope setup, so the *function* can be referenced anywhere; only the values it reads have to be ready by call time. - **Split the scope.** If a block's shadowing declaration is the cause, rename the inner binding rather than relying on the outer one leaking in — it never will. ## The judgment to show A strong answer treats the error as a *feature*. Under `var` the same ordering mistake would have produced `undefined`, and the failure would have surfaced later as `Cannot read properties of undefined` in a different module, or worse, as a request sent to the URL `"undefined"`. The dead zone converts a silent, delayed corruption into an immediate crash naming the exact binding. That is worth saying out loud, because the reflex fix — "switch it back to `var`" — is the one answer that is genuinely wrong.

  • Could you avoid the crash by guarding the access with a typeof check?
    No. Any reference to an uninitialized binding throws, including `typeof config` and `config !== undefined`, so the guard fails on its own line. There is no expression that can safely inspect a dead-zone binding from inside its own scope. The only remedies are ordering ones: move the declaration earlier, or defer the access until after it has run.
  • How does this error differ from `config is not defined`, and why does the difference matter?
    `is not defined` means no binding resolved at all — a typo, a deleted declaration, or the wrong scope. `Cannot access before initialization` means the binding resolved but is uninitialized, so the declaration exists and only the ordering is wrong. Mistaking the second for the first sends you hunting for a missing declaration that is sitting in plain sight a few lines below.
  • Would switching the declaration back to var fix it?
    It would silence the crash and keep the bug. The read would return `undefined` instead of throwing, so the wrong value flows onward and fails later — a property access on undefined, or a request to the literal URL "undefined" — far from the cause. The error is doing its job; fix the ordering, not the declaration keyword.
  • Does this happen with classes as well as const?
    Yes. A `class` declaration creates its binding at scope instantiation but leaves it uninitialized until the class definition is evaluated, so `new Service()` written above `class Service {}` throws the same error. This catches people who generalise from function declarations, which are fully initialized during setup and can be called from anywhere in the scope.

saying these in an interview costs you the question

  • Treats it the same as "x is not defined"
  • Suggests guarding with typeof before the access
  • Reverts to var to make the error go away
  • Only inspects lines above the declaration in source
  • Assumes classes behave like hoisted function declarations

context