ReferenceError: Cannot access '<name>' before initialization в JavaScript

JavaScript Автор: Среда и версия: JavaScript (ECMAScript 2026), ES modules и Node.js
содержание

ReferenceError: Cannot access 'total' before initialization означает, что имя total уже связано с лексической переменной в текущей области видимости, но JavaScript ещё не выполнил её инициализацию. V8 использует эту формулировку в Chrome и Node.js; Firefox и Safari сообщают о той же причине другими словами.

Это не ошибка поиска имени. При ReferenceError: total is not defined подходящего связывания нет вообще. Здесь движок нашёл связывание, но оно ещё не инициализировано. Если же код успел получить undefined, а затем читает свойство, возникает уже TypeError: Cannot read properties of undefined — такой случай разобран в отдельной статье.

JavaScript / 01

Объявление создано раньше, чем появилось значение

Объявление создано раньше, чем появилось значение01 Вход в блок price: не инициализировано Связывание уже существует, но читать его нельзя. 02 Раннее чтение price → ReferenceError Проверяй фактический порядок выполнения. 03 После объявления let price = 100; price → 100 Инициализация завершена: значение доступно.01Вход в блокprice: не инициализированоСвязывание уже существует, но читать его нельзя.02Раннее чтениеprice → ReferenceErrorПроверяй фактический порядок выполнения.03После объявленияlet price = 100;price → 100Инициализация завершена: значение доступно.
Положение строк само по себе ничего не гарантирует: ошибка зависит от того, какая ветка выполнится первой — чтение price или инициализация связывания.

Что такое TDZ и когда она заканчивается

Объявления let, const и class создают лексические связывания. При входе в их область видимости движок уже знает соответствующие имена, но доступ к ним закрыт до инициализации. Этот промежуток называют temporal dead zone, или TDZ — временной мёртвой зоной.

Важно различать создание связывания и выполнение объявления. В следующем блоке total относится ко всему блоку. Связывание существует с момента входа в блок, а значение 42 появляется только при выполнении объявления:

{
  console.log(total)
  const total = 42
}
// ReferenceError: Cannot access 'total' before initialization

У let total; TDZ заканчивается, когда выполнение достигает этой строки: тогда переменная инициализируется значением undefined. У объявления с инициализатором сначала вычисляется выражение справа от =, и лишь затем связывание получает результат. Поэтому самоссылка тоже падает:

const total = total + 1
// ReferenceError: Cannot access 'total' before initialization

TDZ определяется порядком выполнения, а не расстоянием между строками. Функцию можно объявить выше обращения к переменной, если сам вызов произойдёт после инициализации:

function printTotal() {
  console.log(total)
}

const total = 42
printTotal() // 42

Проверка через typeof здесь не поможет. Для имени, которого вовсе нет в области видимости, typeof missingName возвращает строку 'undefined'. Для найденного, но ещё не инициализированного лексического связывания оператор выбрасывает тот же ReferenceError:

console.log(typeof total)
const total = 42
// ReferenceError: Cannot access 'total' before initialization

Быстрая диагностика по стеку

Откройте верхний кадр стека, который относится к вашему исходному коду. Сообщение уже называет связывание: в Cannot access 'config' before initialization искать нужно config, а не любое значение undefined рядом.

Дальше проверьте источник имени:

  1. Найдите ближайшее объявление let, const, class или import с этим именем.
  2. Определите его область видимости: блок, функцию или модуль. Не останавливайтесь на одноимённом объявлении во внешней области.
  3. Восстановите фактический порядок выполнения до строки из стека: ветвления, вызовы функций и инициализаторы важнее положения строк в файле.
  4. Если имя импортировано, проследите цепочку импортов от текущего модуля до модуля-экспортёра. Возврат к уже встреченному файлу означает цикл.

Не вставляйте console.log(config) перед проблемной строкой: это ещё одно чтение в TDZ и оно упадёт раньше. Точка останова и стек вызовов полезнее. По той же причине typeof config не служит безопасной проверкой.

Обращение раньше объявления в том же блоке

Самая короткая причина — выполнение действительно доходит до чтения раньше объявления:

if (shouldRender) {
  render(settings)
}

const settings = loadSettings()

Исправление должно поменять зависимость: сначала получить настройки, затем передать их потребителю.

const settings = loadSettings()

if (shouldRender) {
  render(settings)
}

Если загрузка нужна только на одной ветке, объявление и использование можно перенести вместе внутрь неё. Смысл тот же: код, который потребляет значение, выполняется после кода, который это значение создаёт.

Замена let или const на var не чинит порядок. var заранее получает undefined, поэтому прежнее чтение перестанет падать в этом месте, но потребитель увидит неверное значение. Ошибка либо сместится, либо превратится в тихое нарушение контракта.

Внутреннее имя затенило внешнее

Лексическое объявление действует во всём своём блоке, в том числе выше строки объявления. Поэтому внутренний status скрывает внешний уже при первом обращении:

const status = 'ready'

function report() {
  console.log(status)
  const status = 'pending'
}

report()
// ReferenceError: Cannot access 'status' before initialization

Движок не переходит к внешнему status: имя уже разрешилось во внутреннее связывание. Исправьте роли имён или передайте нужное значение явно:

const appStatus = 'ready'

function report(status) {
  console.log(status)
}

report(appStatus)

При таком исправлении область данных видна из сигнатуры функции, а случайное локальное объявление не меняет источник значения.

Более поздний параметр в значении по умолчанию

Параметры функции инициализируются слева направо. Ранний параметр доступен в значении по умолчанию более позднего, но обратная зависимость попадает в TDZ:

function createRange(start = end - 1, end = 10) {
  return [start, end]
}

createRange()
// ReferenceError: Cannot access 'end' before initialization

Если порядок аргументов менять нельзя, вычислите зависимое значение в теле после инициализации параметров:

function createRange(start, end = 10) {
  const actualStart = start === undefined ? end - 1 : start
  return [actualStart, end]
}

Это редкий источник именно такой формулировки ошибки. Он релевантен, когда верхний кадр стека указывает на заголовок функции, а не на её тело.

Циклические импорты ES modules

Статические импорты связываются до выполнения модулей и остаются живыми ссылками на экспорт. Цикл может пройти загрузку и связывание, но экспорт не станет доступен до того, как модуль-источник его инициализирует.

Пусть a.js запускается как входной модуль:

// a.js
import { b } from './b.js'

export const a = 2
console.log(b)
// b.js
import { a } from './a.js'

export const b = a + 1
// ReferenceError: Cannot access 'a' before initialization

Граф содержит цикл a.js → b.js → a.js. Чтобы вычислить a.js, среда сначала вычисляет его зависимость b.js. Та читает импорт a, но строка export const a = 2 в a.js ещё не выполнена.

Стек обычно указывает на верхнеуровневое чтение или инициализатор в b.js. Найдите экспорт a, затем пройдите обратный путь по импортам. В большом проекте полезно записать только файлы и стрелки: повторившийся файл сразу показывает цикл.

Надёжное исправление — убрать цикл. Общие исходные данные можно вынести в третий модуль, от которого зависят обе стороны:

// values.js
export const a = 2
// b.js
import { a } from './values.js'

export const b = a + 1
// a.js
import { a } from './values.js'
import { b } from './b.js'

console.log(a, b)

Другой вариант — экспортировать функцию и вызывать её после инициализации обоих модулей. Такой вызов должен следовать из жизненного цикла приложения; setTimeout лишь случайно меняет расписание и оставляет цикл скрытой зависимостью.

Цикл отличается от ошибки разрешения. Если Node.js не может найти файл или пакет по спецификатору, модуль не доходит до выполнения; диагностика такого случая описана в Cannot find module. Если файл найден, но именованный экспорт отсутствует, связывание модуля обычно завершается SyntaxError. require is not defined тоже возникает после разрешения файла, но означает, что ESM‑код обратился к CommonJS‑загрузчику. Cannot access … before initialization появляется позже, при выполнении уже связанного графа.

Что исправлять в первую очередь

Для локального имени поставьте создание значения раньше его первого фактического потребителя или уменьшите область видимости так, чтобы объявление и использование находились в одной ветке. При затенении переименуйте роли либо передайте значение параметром. Для параметров по умолчанию оставляйте зависимости слева направо или переносите вычисление в тело функции. Для импортов меняйте граф: выносите общую основу в модуль без обратной зависимости и убирайте верхнеуровневое чтение из цикла.

Не маскируйте причину переходом на var, запасным undefined или задержкой по таймеру. После исправления должно быть ясно, какая операция создаёт значение и почему любой потребитель запускается только после неё.

Источники