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 — такой случай разобран в отдельной статье.
Объявление создано раньше, чем появилось значение
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 рядом.
Дальше проверьте источник имени:
- Найдите ближайшее объявление
let,const,classилиimportс этим именем. - Определите его область видимости: блок, функцию или модуль. Не останавливайтесь на одноимённом объявлении во внешней области.
- Восстановите фактический порядок выполнения до строки из стека: ветвления, вызовы функций и инициализаторы важнее положения строк в файле.
- Если имя импортировано, проследите цепочку импортов от текущего модуля до модуля-экспортёра. Возврат к уже встреченному файлу означает цикл.
Не вставляйте 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 или задержкой по таймеру. После исправления должно быть ясно, какая операция создаёт значение и почему любой потребитель запускается только после неё.