You are looking at one of these, in Chrome, Edge or Node.js:
ReferenceError: userName is not defined
ReferenceError: Cannot access 'price' before initializationThe first means the engine searched every scope from the current line outwards and found no variable called userName at all. That is different from a variable that exists and holds the value undefined, which never throws when you read it. The second message means the variable does exist in scope, but you touched it before its let, const or class declaration ran.
Firefox reports the same first message and can't access lexical declaration 'price' before initialization for the second. Safari says Can't find variable: userName and Cannot access uninitialized variable.
Quick fix checklist
- Compare the name in the message with the declaration character by character: case matters (
userNamevsusername). - Check that the variable is declared in the same block or an enclosing one, not inside a sibling
if, loop or function. - For
Cannot access ... before initialization, move the declaration above the first use or delay the use until after it. - For
window,documentorlocalStorage is not defined, your code is running in Node (a test runner, SSR or a build step), not a browser. - For
require is not defined in ES module scope, useimport, orcreateRequireif you must load CommonJS. - For browser libraries (
$,Chart,google), check that their script tag loads before your code uses them.
Before you start
You should know the three declaration keywords and their basic differences, and what a block is (anything between { and }). The examples run in Node.js 18 or newer; the script-order section applies to browsers.
Why it happens
JavaScript resolves names through a chain of scopes: the current block, the enclosing function, the module, then the global object. A ReferenceError is thrown when that lookup fails, or when it finds a binding that is not yet initialised. Compare the outcomes:
let declared;
console.log(declared); // declared, no value
console.log(typeof notDeclared); // typeof is safe on undeclared names
try { console.log(notDeclared); } catch (e) { console.log(String(e)); }
const userName = 'Ana';
try { console.log(username); } catch (e) { console.log(String(e)); }
if (true) { const total = 10; }
try { console.log(total); } catch (e) { console.log(String(e)); }
try { console.log(price); let price = 5; } catch (e) { console.log(String(e)); }
// undefined
// undefined
// ReferenceError: notDeclared is not defined
// ReferenceError: username is not defined
// ReferenceError: total is not defined
// ReferenceError: Cannot access 'price' before initializationUndeclared versus undefined. declared exists and has the value undefined. notDeclared has no binding. typeof is the one operator that tolerates a missing binding, which is why feature detection uses it.
Block scope. let and const live only inside the block where they are declared. total vanished when the if block ended. var would have leaked out to the function, which is one reason older code did not hit this error.
The temporal dead zone (TDZ). let, const and class declarations are hoisted, so the engine knows price belongs to that block, but they stay uninitialised until the declaration line executes. Any access in between throws. This also catches functions that run too early:
function getRate() { return rate; }
try { getRate(); } catch (e) { console.log(String(e)); }
const rate = 0.2;
console.log(getRate());
// ReferenceError: Cannot access 'rate' before initialization
// 0.2The function itself is fine; what matters is when it runs relative to the const line.
Strict mode turns typos into errors. In a classic sloppy-mode script, assigning to an undeclared name silently creates a global. ES modules and classes are always strict, so the same typo throws:
function setCount() { cuont = 5; } // typo, inside an ES module
try { setCount(); } catch (e) { console.log(String(e)); }
// ReferenceError: cuont is not definedEnvironment globals. window, document and localStorage are provided by browsers. require, module and __dirname are provided by Node’s CommonJS wrapper. None of them is part of the language, so each is missing somewhere. In an ES module, Node adds a hint:
ReferenceError: require is not defined in ES module scope, you can use import instead
This file is being treated as an ES module because it has a '.js' file extension and
'/app/package.json' contains "type": "module". To treat it as a CommonJS script,
rename it to use the '.cjs' file extension.globalThis is the portable name for the global object in every environment.
Script load order (browser). Classic scripts run in the order they appear. If your app.js tag comes before the tag that loads jQuery or Chart.js, $ or Chart is not defined yet when your code runs. Adding defer to both keeps their relative order while waiting for the HTML to parse; async does not preserve order and can reintroduce the bug.
Step-by-step walkthrough
Step 1: Classify the message
Two different problems share the ReferenceError type. x is not defined is a lookup failure: the name is wrong, out of scope or not provided by this environment. Cannot access 'x' before initialization is an ordering problem: the name is right but used too early. Knowing which one you have halves the search.
Step 2: Locate the line and the environment
The top stack frame gives the file and line. Then ask where that code runs. A browser-only module imported by a Node test runner, a server-side renderer or a build script will hit window is not defined even though it works in the browser. The stack trace path (node:internal/... frames, a .next/server or dist/server folder) usually gives this away.
Step 3: Find the declaration and compare scopes
Search for the declaration. If it is inside a different block, function or file, decide where the value should live: return it, pass it as a parameter, or export and import it. If the declaration is missing entirely and the name is a library global, check how the library is loaded.
Step 4: Fix ordering problems at the source
For the TDZ, reorder so the declaration runs first, or wrap the dependent code in a function called later. For circular imports between modules, a TDZ error often means module A reads a binding from module B while B is still evaluating; breaking the cycle or moving the read into a function fixes it.
Worked scenario
A team adds server-side rendering to an app. A small theme helper that has worked in the browser for a year now crashes the server on startup:
// theme.js, shared by the browser bundle and the server renderer
const saved = window.localStorage.getItem('theme'); // runs as soon as the module loads
export function initialTheme() {
return saved ?? 'light';
}Importing it in Node:
try {
await import('./theme.js');
} catch (e) {
console.log(String(e));
}
// ReferenceError: window is not definedDiagnosis. Nothing is misspelled. The module touches window at the top level, which runs the moment anything imports it. The server has no window, so the import fails before any function is called. Wrapping the import in try/catch or declaring var window = {} would hide the problem and render the wrong theme.
Fix. Move environment access into a function and detect the environment with typeof, which is safe even when window has no binding:
export function initialTheme() {
if (typeof window === 'undefined') return 'light'; // server: no storage to read
try {
return window.localStorage.getItem('theme') ?? 'light';
} catch {
return 'light'; // storage can be blocked in private modes
}
}The server now renders the default, and the browser reads the saved preference during hydration or on first interaction.
Common mistake
The tempting fix for x is not defined is to declare x somewhere global so the error stops: window.config = ... in one file and bare config in another, or var window = {} in a server bundle. That couples files through hidden globals, breaks under ES modules (where top-level declarations are module-scoped, not global) and turns a loud error into silent wrong behaviour.
The second tempting fix, for the TDZ, is to switch let back to var. The error disappears because var is initialised to undefined at hoisting time, so your code now runs with undefined instead of failing. The ordering bug is still there.
Verify the behavior
Test the helper in all three environments it can meet: server, browser and a browser with storage blocked. Save next to theme.mjs (the fixed module) as verify.mjs:
import assert from 'node:assert/strict';
import { initialTheme } from './theme.mjs';
// 1. Server: no window at all.
assert.equal(typeof window, 'undefined');
assert.equal(initialTheme(), 'light');
// 2. Browser-like: fake a window with a stored preference.
globalThis.window = { localStorage: { getItem: (key) => (key === 'theme' ? 'dark' : null) } };
assert.equal(initialTheme(), 'dark');
// 3. Storage blocked: getItem throws.
globalThis.window = { localStorage: { getItem() { throw new Error('SecurityError'); } } };
assert.equal(initialTheme(), 'light');
delete globalThis.window;
console.log('theme works on server and client');
// theme works on server and clientFor browser load-order bugs, open DevTools, go to the Network panel, filter by JS, and confirm the library request completes before your script; or type the global’s name ($, Chart) in the console after the page loads.
Interview exercise
What does this print, and why is the error message different on each line?
console.log(typeof a);
console.log(typeof b);
let b = 1;Answer and reasoning
The first line prints undefined: a has no binding anywhere, and typeof is specified to return 'undefined' for unresolvable references instead of throwing. The second line throws ReferenceError: Cannot access 'b' before initialization. b is declared with let later in the same scope, so the engine already created the binding during hoisting, but it is in the temporal dead zone until line three executes. typeof gets no special treatment there, because the reference does resolve; it resolves to an uninitialised binding. The interviewer is checking whether you understand that let and const are hoisted (the binding exists for the whole scope) but not initialised, which is exactly what makes the TDZ error possible. With var b, the second line would print undefined.
Continue learning
Drill these rules with the JavaScript interview questions and the JavaScript MCQs. The TDZ and hoisting are covered step by step in hoisting, the TDZ and var, let and const, and circular-import TDZ errors connect to module live bindings. If require or import is the name that is missing, read Cannot use import statement outside a module. Official references: MDN’s ReferenceError: “x” is not defined and can’t access lexical declaration before initialization.