TypeError: Cannot read properties of undefined (reading 'name') является самой частой строкой в консоли браузера и одной из тех, что чаще всего трактуют неверно. Почти каждый, кто её видит, начинает искать то, что называется name. С самим name всё в порядке. Ошибка на самом деле о том, что должно было его содержать.

Именно в этом вся сложность сообщения, если сформулировать одной фразой. Оно называет свойство, которое вы пытались прочитать, и ничего не говорит о переменной, которая оказалась пустой. Поэтому исправление никогда не находится там, куда указывает сообщение, и первая минута любого расследования уходит на то, чтобы понять, о чём сообщение говорит на самом деле.

Как читать сообщение

Возьмём код user.profile.name. Если user существует, но у него нет profile, тогда user.profile равен undefined, и обращение к .name у undefined выбрасывает исключение. Сообщение гласит (reading 'name'). Оно не говорит, что отсутствует profile, а name, единственное слово во всём выражении, к проблеме как раз отношения не имеет.

Разные браузеры формулируют это по-разному, и это важно, когда об одном и том же баге сообщают три разных человека:

  • Chrome и Edge: Cannot read properties of undefined (reading 'name'). В старых версиях писали Cannot read property 'name' of undefined, смысл тот же.
  • Firefox: TypeError: user.profile is undefined или can't access property "name", user.profile is undefined. Firefox называет именно то выражение, которое оказалось пустым, и это полезнее.
  • Safari: TypeError: undefined is not an object (evaluating 'user.profile.name'). Safari показывает выражение целиком и оставляет вам разбираться, какая его часть подвела.

Три формулировки, один баг. Команда, которая заводит тикеты, просто вставляя строку из консоли, откроет три разных тикета, и только формулировка Firefox подскажет, куда смотреть.

У этой ошибки есть родственная, Cannot read properties of null, и это не совсем то же самое. null вы получаете, когда поиск выполнился, но ничего не нашёл: например, document.querySelector('.total') на странице без элемента .total. undefined вы получаете, когда что-то вообще не было задано: свойство, которого нет у объекта, переменная, объявленная, но не получившая значения, функция, которая ничего не вернула. null обычно указывает на страницу; undefined обычно указывает на данные.

Откуда берётся undefined

Сообщение во всех случаях одинаковое. Причина же одна из небольшого списка, и имя свойства в скобках неплохо подсказывает, какая именно.

Данные ещё не пришли. Страница отрисовалась раньше, чем вернулся запрос, который должен был её наполнить, и код прочитал response.items.length, пока response был ещё заглушкой. Это тот случай, который проявляется на медленном соединении и не проявляется на быстром, на телефоне и не на ноутбуке, у заказчика и никогда у разработчика. (reading 'length') и (reading 'map'), типичные приметы: что-то ожидало массив, а получило пустоту.

Изменилась структура данных. API раньше всегда возвращал user.profile; теперь он возвращает user.profile только для аккаунтов, которые его заполнили, либо переименовал ключ, либо поле переехало на уровень выше. На клиенте ничего не менялось, а ошибка началась в день чужого деплоя. Это тот вариант, который приходит с формулировкой «вчера же всё работало».

Ключ называется по-другому. В коде item.userId, в ответе сервера item.user_id. Никакого предупреждения не будет: JavaScript читает несуществующее свойство как undefined и продолжает работу, а ошибка всплывает лишь на шаг позже, когда что-то пытается прочитать значение через него.

Список пуст. results[0].title, когда results равен []. Абсолютно рабочий код, пока не случится первый поиск без совпадений. Тестовые данные редко бывают пустыми. Боевые данные, часто.

Что-то вернуло пустоту. Функция, в которой один из путей выполнения забывает сделать return, async-функция, вызывающий код которой забыл поставить await и получил промис вместо значения, .find(), который ничего не нашёл. Все эти случаи выдают undefined и передают его дальше, на следующую строку.

Код минифицирован. В продакшене сообщение выглядит как Cannot read properties of undefined (reading 'a'), потому что сборка переименовала свойство. Номер строки указывает куда-то в одну гигантскую строку. Без source map сообщение вообще перестаёт что-либо говорить, и остаётся только цепочка запросов и кликов, которая к этому привела.

Как это выглядит с другой стороны

Человеку, который столкнулся с этой ошибкой, ничего из этого не видно. Он видит участок страницы, который так и не заполнился, кнопку, которая ничего не делает, форму, которая не отправляется. Нигде на странице не написано «произошла ошибка», потому что ошибка случилась внутри скрипта, и скрипт просто остановился. Строка в консоли существует, но в панели, которую он никогда не открывал.

Поэтому в отчёте написано «страница заказа пустая». А разработчик воспроизводит это на быстром соединении, с полностью заполненным аккаунтом и непустым списком, и страница вовсе не пустая.

Слабо
Страница заказа пустая у одного клиента, воспроизвести не получается.
Лучше
Консоль: TypeError: Cannot read properties of undefined (reading 'items'), orders.js line 214. Запрос к /api/orders непосредственно перед этим вернул 200 с пустым телом. У клиента пока нет заказов.

Второй вариант, это уже готовое решение, которое ждёт своего часа. Всё в нём взято со страницы в момент сбоя: строка из консоли, предшествующий запрос и ответ, который должен был содержать данные. Это как раз те доказательства, которые нужны для этой ошибки, и именно их человек, смотрящий на пустую страницу, самостоятельно предоставить не может. Наш гайд по написанию баг-репортов подробно разбирает, что нужно запрашивать; короткая версия в том, что для этой ошибки консоль и сетевой лог, это и есть отчёт, а описание, всего лишь подпись к нему.

Session Replay

Бесплатное расширение для Chrome. Один клик на странице, которая ведёт себя не так, как нужно, и оно сохранит скриншот, консоль и сетевой лог, а затем выдаст ссылку, которую можно вставить в тикет.

Установить расширение

Как найти причину в инструментах разработчика

Когда страница у вас перед глазами и ошибка воспроизводится, разобраться помогают три приёма.

Остановка на исключениях. В панели Sources в Chrome иконка паузы с подписью «Pause on uncaught exceptions» останавливает скрипт ровно на той строке, где произошло исключение, и все переменные при этом остаются в области видимости. Наведите курсор на выражение, и вы увидите, какая именно часть user.profile.name равна undefined, а сообщение об этом само по себе никогда бы не сказало.

Посмотрите на предшествующий запрос. Переключитесь на вкладку Network и найдите ответ, который должен был наполнить объект данными. В девяти случаях из десяти ответ на вопрос уже там: пустое тело, 200 с сообщением об ошибке внутри, ключ с другим названием или запрос, который вообще не отправился, потому что ждал чего-то ещё. Статья про failed to fetch разбирает случай, когда сам запрос не выполнился; эта статья про случай, когда он выполнился успешно, но вернул данные не той формы.

Проверьте порядок событий. Если ошибка появляется лишь иногда, почти всегда дело в первой из причин выше: чтение выполняется раньше, чем появляются данные. Ограничьте скорость сети во вкладке Network до «Slow 3G» и перезагрузите страницу. Если ошибка теперь воспроизводится стабильно, значит, это состояние гонки, и правильное решение, дождаться данных, а не просто защититься от чтения.

Исправление, которое не исправляет

user?.profile?.name заставляет ошибку исчезнуть. Но это не заставляет профиль появиться. Опциональная цепочка превращает падение скрипта в тихий undefined, который затем просачивается на страницу в виде пустой строки, отсутствующей строки таблицы, кнопки без подписи, и теперь баг невидим для вас, но по-прежнему виден клиенту.

Это правильный инструмент, когда отсутствие значения законно: аккаунт, у которого действительно ещё нет профиля. И это неправильный инструмент, когда отсутствие значения и есть баг. Прежде чем добавлять ?., стоит спросить себя, должно ли это значение вообще когда-либо отсутствовать. Если нет, падение скрипта пыталось вам что-то сказать, а защита, это способ не слушать.

Что из этого следует

Сообщение называет свойство, которое вы запросили, и прячет то, что оказалось пустым. Читайте его как «шаг перед этим ничего не произвёл», и идите смотреть именно этот шаг: запрос, возвращаемое значение, название ключа, список, который оказался пустым. Исправление всегда лежит раньше по потоку, чем указанный номер строки.

А когда отчёт приходит с чужого экрана, строка из консоли и сетевой лог, это не контекст к багу. Для этой ошибки они и есть баг.