Cannot read properties of null означает, что выражение слева от точки вернуло null, а код сразу попытался прочитать свойство или вызвать метод. Для DOM чаще всего элемент не найден по селектору. Проверьте значение отдельно, сравните селектор с HTML и убедитесь, что скрипт выполняется после создания элемента. Оператор ?. может предотвратить падение, но не объясняет, почему нужного узла нет.
Типичная строка выглядит коротко: document.querySelector("#save").addEventListener(...). В ней спрятаны две операции. Сначала браузер ищет элемент, затем JavaScript вызывает метод у результата. Если поиск вернул null, ошибка упоминает уже свойство addEventListener, хотя первопричина находится в селекторе, порядке загрузки или условном рендеринге.
Сохраните результат querySelector в переменную и выведите его.
Проверьте символ # для id, . для class и точное написание.
Подключите скрипт с defer или после нужной разметки.
Откуда берётся null в DOM-коде
Метод querySelector возвращает первый элемент, совпавший с CSS-селектором, либо null, если совпадений нет. Это нормальный контракт поиска, а не сбой браузера. Ошибка появляется на следующей операции, когда код ведёт себя так, будто результат гарантированно является элементом.
const button = document.querySelector('#save');
console.log(button); // null
button.addEventListener('click', saveDraft);
// TypeError: Cannot read properties of null
// (reading 'addEventListener')| Наблюдение | Вероятная причина | Проверка |
|---|---|---|
В консоли null | Селектор никого не нашёл | Найти тот же селектор во вкладке Elements |
| Элемент виден, но код падает | Скрипт стартовал слишком рано | Проверить позицию script и defer |
| Падает только на одной странице | Разметка там отсутствует | Сделать инициализацию условной |
| Падает после перерисовки | Старая ссылка больше не соответствует DOM | Повторить поиск после обновления |
| Селектор вызывает SyntaxError | Сам CSS-селектор некорректен | Исправить экранирование специальных символов |

Разделение цепочки показывает, что ошибка начинается с результата поиска, а проявляется на следующем методе.
console.log({ button }). Если там элемент, ошибка относится к другому объекту в выражении. Если null, у вас три главные гипотезы: неверный селектор, ранний запуск или отсутствие узла в конкретном состоянии страницы.Как проверить селектор и порядок загрузки
- Откройте страницу, на которой воспроизводится ошибка, и вкладку Elements.
- Найдите ожидаемый элемент по тексту или структуре разметки.
- Скопируйте селектор из кода и выполните
document.querySelector(...)в Console. - Сверьте регистр, дефисы,
idиclassпосимвольно. - Поставьте точку останова на проблемной строке и проверьте готовность DOM.
- После одной правки перезагрузите страницу без использования старого состояния.
Самый простой стабильный вариант для обычного HTML состоит в атрибуте defer. Браузер загружает внешний файл параллельно, но выполняет его после разбора документа и до события DOMContentLoaded. Если скрипт подключён в конце body, нужная разметка тоже обычно уже создана. Не смешивайте оба способа без причины.
<head>
<script src="app.js" defer></script>
</head>
<body>
<button id="save" type="button">Сохранить</button>
</body>
<!-- app.js -->
const button = document.querySelector('#save');
button.addEventListener('click', () => console.log('Готово'));
defer запускает внешний скрипт после разбора документа, когда статические элементы уже созданы.
После добавления defer снова проверьте переменную. Если она остаётся null, проблема не во времени: селектор не совпадает с разметкой либо элемента нет на этой странице. Хорошая диагностика меняет только одну гипотезу за запуск.
Когда нужна проверка, optional chaining или ошибка
Не каждый отсутствующий элемент является багом. Общий файл может подключаться на десяти страницах, а кнопка существовать только на странице редактора. Тогда условная инициализация уместна. Но если форма обязана иметь поле, молчаливый ?. скрывает сломанную разметку. Выберите поведение по контракту интерфейса.
| Контракт | Код | Поведение |
|---|---|---|
| Элемент необязателен | element?.addEventListener(...) | Ничего не делать при отсутствии |
| Нужен блок инициализации | if (element) { ... } | Явно ограничить связанные действия |
| Элемент обязателен | if (!element) throw new Error(...) | Упасть с понятной причиной |
| Элементов несколько | querySelectorAll | Получить пустой NodeList, а не null |
| Узел создаётся позже | Искать после рендера | Не хранить преждевременную ссылку |
const form = document.querySelector('#profile-form');
if (!form) {
throw new Error('Не найден обязательный #profile-form');
}
form.addEventListener('submit', (event) => {
event.preventDefault();
console.log('Форма отправлена');
});
Условие, optional chaining и явная ошибка решают разные задачи.
null. Для необязательного виджета условие, наоборот, является штатным ветвлением.Четыре исправления, которые часто маскируют баг
Случайное добавление задержки может создать иллюзию исправления на быстром компьютере и снова сломаться при медленной сети. Замена селектора на первый попавшийся тег привязывает код не к смыслу, а к текущей форме страницы. Надёжное решение выражает ожидание явно: когда узел должен существовать и как его однозначно найти.
Время загрузки не является контрактом. Используйте defer, событие или сигнал завершения рендера.
Ошибки исчезают из консоли, но обязательный обработчик так и не подключается.
button может вернуть не ту кнопку. Используйте устойчивый id или data-атрибут.
После замены DOM старый элемент может быть отключён. Ищите его в актуальный момент.
Проверьте полную перезагрузку, страницу без необязательного блока и состояние после динамической перерисовки. Если код корректно различает обязательный и необязательный элемент, ошибка не вернётся при небольшом изменении интерфейса.
Практика: надёжно подключаем кнопку сохранения
Соберите минимальную страницу из кнопки и внешнего скрипта. Сначала намеренно ошибитесь в id и зафиксируйте null. Затем исправьте селектор, добавьте defer и собственную проверку обязательного элемента. Такая практика показывает все три слоя проблемы без большого приложения.
- Результат каждого
querySelectorсохранён в переменную с понятным именем. - Селектор выполняется в Console и возвращает ожидаемый узел.
- Скрипт подключён с
deferили после разметки. - Для обязательного узла есть явная ошибка, для необязательного условие.
- Страница проверена после полной перезагрузки и в состоянии без данных.
- В Console нет новых TypeError после клика и отправки формы.
(reading "addEventListener") называет операцию, которую пытались выполнить у null. Ищите объект непосредственно слева от этой операции, а не любой похожий селектор в проекте.Что возвращает querySelector, если совпадений нет?
Значение null. Сам вызов поиска при этом корректен.
Почему ошибка говорит про addEventListener?
Поиск уже вернул null, а следующая операция попыталась прочитать у него метод addEventListener.
Когда подходит optional chaining?
Когда отсутствие элемента является допустимым состоянием и код действительно можно пропустить.
Чем defer полезен для DOM-кода?
Внешний скрипт выполняется после разбора HTML, поэтому находящиеся в разметке элементы уже созданы.
Почему setTimeout ненадёжен?
Фиксированная задержка не гарантирует состояние DOM на другом устройстве или при другой скорости сети.
В курсе JavaScript сначала проверяйте каждый селектор в консоли, а затем добавляйте событие. Продолжите материалом про DOM-манипуляции и разбором почему кнопка не нажимается.
Для целой страницы пригодится руководство по созданию сайта на HTML, CSS и JavaScript. В Кодике удобно сохранить минимальный HTML и JS рядом, чтобы видеть, какой селектор относится к какому узлу.