ReferenceError: window is not defined и ReferenceError: document is not defined означают одно: код обратился к браузерному глобальному объекту там, где его нет. Чаще всего файл выполняется в Node.js во время SSR, сборки или теста. Та же ошибка возможна в Web Worker: это браузерная среда, но у воркера нет ни window, ни DOM.
window и document не входят в сам язык JavaScript. Их предоставляет браузер для кода страницы: window представляет окно и служит его глобальным объектом, а window.document указывает на загруженный DOM-документ. Node.js предоставляет другой набор глобальных объектов. Поэтому один и тот же синтаксически верный модуль может работать в странице и падать на сервере.
document существует там, где создан DOM
Сначала выясните, где выполняется код
Не начинайте с добавления проверки вокруг каждой строки. Откройте верхний кадр стека, который относится к вашему коду, и ответьте на два вопроса: какой процесс выполняет файл и на какой фазе возникает ошибка.
Типичные среды различаются так:
- в главном окне браузера доступны
windowиdocument; - в Web Worker нет окна страницы и DOM, хотя код выполняет браузер;
- в Node.js есть
globalThisи набор глобальных API Node.js, но DOM-документ сам по себе не появляется; - при SSR компонент сначала вычисляется на сервере, а затем страница подключает клиентский JavaScript;
- сборщик может выполнять конфигурацию, плагины, генерацию страниц или серверную часть приложения в Node.js;
- тестовый раннер использует выбранную среду: часто Node.js по умолчанию, а DOM-эмулятор — только по настройке.
Наличие отдельного Web API ещё не доказывает, что код работает в браузере. Например, Node.js поддерживает fetch, URL и AbortController, но это не добавляет к процессу окно страницы или DOM.
Для первичной проверки достаточно typeof: этот оператор не выбрасывает ReferenceError для необъявленного имени.
const runtime = {
hasWindow: typeof window !== 'undefined',
hasDocument: typeof document !== 'undefined',
}
console.log(runtime)
Проверка показывает возможности текущего места выполнения, а не тип всего проекта. В универсальном приложении сервер и браузер выполняют разные части одного графа модулей, поэтому результат закономерно меняется.
globalThis не создаёт единый набор API для всех сред. Это общее имя глобального объекта, а не полифил DOM. В обычном Node.js выражение globalThis.document не даёт настоящий документ: свойства может не быть. Замена document на globalThis.document лишь меняет ранний ReferenceError на undefined и часто переносит сбой к следующему обращению к свойству.
Проверьте фазу: импорт, рендер или клиентское событие
Верхнеуровневый код модуля выполняется при первом импорте. Поэтому безобидный на вид импорт может уронить SSR или тест ещё до вызова вашей функции:
// shortcuts.js
document.addEventListener('keydown', handleShortcut)
export function handleShortcut(event) {
// ...
}
// server-entry.js
import './shortcuts.js'
export function renderPage() {
// До этой функции выполнение не дойдёт.
}
Статические зависимости выполняются до тела импортирующего модуля. Поэтому поздняя проверка в server-entry.js не защитит побочный эффект, который уже произошёл внутри shortcuts.js.
Та же граница важна в компонентах. Обращение к window или document во время вычисления компонента выполняется и при серверном рендере. Обработчик клика, клиентский эффект или функция инициализации запускаются позже, уже в странице. Сначала определите, нужен ли браузерный результат для исходного HTML. Если нет, перенесите действие на клиентскую фазу. Если нужен, серверу требуется отдельный источник данных, а не фиктивный DOM.
Используйте typeof window, когда отсутствие браузера допустимо
Безопасная проверка необъявленного глобального имени выглядит так:
export function getViewportWidth() {
if (typeof window === 'undefined') {
return null
}
return window.innerWidth
}
Проверка if (window) не работает: чтобы вычислить условие, движок сначала должен разрешить имя window и сразу выбрасывает ReferenceError. typeof window возвращает строку 'undefined', если имени нет.
Такая проверка уместна, только когда контракт допускает отсутствие браузерного значения. Например, сервер может вернуть null, а клиент измерит область просмотра после подключения. Если функция обязана менять DOM, молча пропускать работу опасно: вызывающий код решит, что операция прошла. Для такой функции лучше явно ограничить место вызова или передать зависимость параметром.
Не проверяйте только window, если дальше нужен document. В некоторых средах и тестовых заглушках набор глобальных свойств неполон. Проверяйте именно используемую возможность:
export function findAppRoot() {
if (typeof document === 'undefined') {
return null
}
return document.querySelector('[data-app-root]')
}
Уберите браузерные побочные эффекты из импорта
Модуль, который лишь объявляет функции, безопаснее импортировать на сервере, в сборщике и в тесте. Браузерную работу запускает отдельная клиентская точка входа:
// shortcuts.js
export function startShortcuts(doc) {
function handleShortcut(event) {
// ...
}
doc.addEventListener('keydown', handleShortcut)
return function stopShortcuts() {
doc.removeEventListener('keydown', handleShortcut)
}
}
// client-entry.js
import { startShortcuts } from './shortcuts.js'
if (typeof document !== 'undefined') {
const stopShortcuts = startShortcuts(document)
window.addEventListener('pagehide', (event) => {
if (!event.persisted) stopShortcuts()
})
}
Так импорт не подписывается на события сам, а функция возвращает симметричную очистку. При сохранении страницы в bfcache подписка остаётся: после возврата из кэша клиентский модуль не запускается заново. Клиентский жизненный цикл конкретного фреймворка должен запускать и останавливать её в подходящий момент.
Например, эффекты React выполняются только на клиенте, поэтому подписку можно поставить в useEffect. Рендер компонента при этом остаётся чистым и не читает DOM:
import { useEffect } from 'react'
import { startShortcuts } from './shortcuts.js'
export function Editor() {
useEffect(() => startShortcuts(document), [])
return null
}
Это пример границы жизненного цикла, а не универсальная команда для любого фреймворка. В другой системе используйте клиентский хук монтирования или гидрации. Не переносите в эффект вычисления, которые должны участвовать в серверном HTML: эффект запускается позже.
Если сторонняя библиотека обращается к DOM прямо при импорте, вынесите и сам импорт на клиентскую фазу:
export async function startBrowserWidget() {
if (typeof window === 'undefined') {
return null
}
const { mountWidget } = await import('./browser-widget.js')
return mountWidget(document.querySelector('[data-widget]'))
}
Динамический импорт нужен только из-за побочного эффекта зависимости. Для собственного модуля проще удалить действие верхнего уровня и экспортировать обычную функцию инициализации.
Передавайте DOM-зависимости параметрами
Код легче тестировать, когда его требования видны в сигнатуре. Функции форматирования, расчёта и валидации обычно вообще не должны знать о document. Считайте данные отдельно, а DOM обновляйте в тонком браузерном адаптере:
export function formatCartCount(items) {
return `Товаров: ${items.length}`
}
export function renderCartCount(doc, items) {
const output = doc.querySelector('[data-cart-count]')
if (!output) {
throw new Error('Не найден элемент [data-cart-count]')
}
output.textContent = formatCartCount(items)
}
Тест formatCartCount() не требует DOM. Для адаптера можно передать документ из браузера или из специализированной тестовой среды. Такой параметр полезнее глобальной заглушки: зависимость видна вызывающему коду и не влияет на соседние тесты.
Для API, который используется и на сервере, и в браузере, передавайте небольшой объект с нужными операциями. Не копируйте весь window:
export function saveDraft(storage, value) {
storage.setItem('draft', value)
}
saveDraft(window.localStorage, editorValue)
В серверном сценарии вызывающий код может не сохранять черновик либо передать серверное хранилище с тем же требуемым методом. Выбор становится частью архитектуры приложения, а не случайной проверкой глобального имени внутри бизнес-логики.
Не включайте DOM-эмулятор для каждого теста
Среда Node.js полезна тем, что обнаруживает неявную зависимость от браузера. Если тест проверяет расчёт, парсер, запрос к API или серверный рендер, оставьте его без window и document. Случайное обращение к DOM тогда завершит тест именно в месте архитектурной ошибки.
DOM-эмулятор нужен, когда предмет теста — поведение DOM: поиск и создание элементов, изменение атрибутов, распространение событий. В Vitest такую среду можно выбрать для отдельного файла:
// @vitest-environment jsdom
import { describe, expect, it } from 'vitest'
import { renderCartCount } from './cart.js'
describe('renderCartCount', () => {
it('записывает количество в найденный элемент', () => {
document.body.innerHTML = '<output data-cart-count></output>'
renderCartCount(document, [{ id: 1 }, { id: 2 }])
expect(document.querySelector('[data-cart-count]').textContent).toBe('Товаров: 2')
})
})
Эмулятор не превращает тест в настоящий браузер. Геометрия, отрисовка, часть навигации и некоторые Web API могут отличаться или отсутствовать. Подключайте его ради проверяемого DOM-контракта, а не чтобы ошибка исчезла во всём наборе тестов.
Что не исправляет причину
Не добавляйте пустой объект в глобальную область:
globalThis.window = {}
globalThis.document = {}
У таких объектов нет контрактов Window и Document. Код упадёт позже на addEventListener, querySelector, location или другом свойстве, а стек станет дальше от причины. Полный DOM-эмулятор оправдан в DOM-тесте; в продакшен-сервере он обычно маскирует неверно выбранную среду и добавляет лишнюю работу.
Не лечит ошибку и замена системы модулей сама по себе. Если рядом появляется require is not defined, это ещё один признак, что смешались ожидания разных сред или форматов модулей; порядок диагностики описан в статье require is not defined. Но переход с CommonJS на ESM не создаёт DOM в Node.js.
Итоговая проверка короткая: найдите первое обращение к браузерному API, установите фактическую среду и фазу выполнения, затем выберите одну границу. Необязательное чтение защищайте через typeof, побочный эффект запускайте в клиентском жизненном цикле, а обязательную браузерную зависимость передавайте явно. Тогда серверный код не притворяется браузером, а браузерный код выполняется там, где действительно существуют Window и Document.