
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, который затем просачивается
на страницу в виде пустой строки, отсутствующей строки таблицы, кнопки без подписи, и теперь баг
невидим для вас, но по-прежнему виден клиенту.
Это правильный инструмент, когда отсутствие значения законно: аккаунт, у которого действительно
ещё нет профиля. И это неправильный инструмент, когда отсутствие значения и есть баг. Прежде чем
добавлять ?., стоит спросить себя, должно ли это значение вообще когда-либо отсутствовать. Если
нет, падение скрипта пыталось вам что-то сказать, а защита, это способ не слушать.
Что из этого следует
Сообщение называет свойство, которое вы запросили, и прячет то, что оказалось пустым. Читайте его как «шаг перед этим ничего не произвёл», и идите смотреть именно этот шаг: запрос, возвращаемое значение, название ключа, список, который оказался пустым. Исправление всегда лежит раньше по потоку, чем указанный номер строки.
А когда отчёт приходит с чужого экрана, строка из консоли и сетевой лог, это не контекст к багу. Для этой ошибки они и есть баг.