Виды ошибок и объект Error
Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.
Когда что-то ломается, JavaScript не просто останавливается — он бросает объект, у которого есть вид, сообщение и след из вызовов. Научитесь читать эти три поля, и половина отладки превратится в чтение готового диагноза.
Ошибка в JavaScript — такой же объект, как массив или дата. Его создаёт конструктор Error (или один из его наследников), и у него есть три поля, которые вы будете читать постоянно: name — вид ошибки, message — описание для человека, stack — след из вызовов, приведших к поломке.
Обратите внимание: создать объект ошибки — ещё не значит сломать программу. Пока он просто лежит в переменной, всё идёт своим чередом. Прерывает выполнение throw — и ровно то же самое делает движок, когда натыкается на недопустимую операцию, только объект ошибки он создаёт сам.
Error — общий предок, его бросают, когда ничего конкретнее не подходит. Остальные виды наследуются от него и говорят о характере беды: сам движок почти всегда выбирает специализированный конструктор, а не голый Error. Всего встроенных классов ошибок девять; пять из таблицы ниже закрывают почти всё, что встретится в повседневном коде, а EvalError, URIError, AggregateError и SuppressedError попадаются редко и в особых местах.
| Вид | Когда бросается | Типичный пример |
|---|---|---|
| Error | Общий случай, обычно бросает ваш собственный код | throw new Error("Задача не найдена") |
| TypeError | Значение есть, но операция для него бессмысленна | вызов не-функции, чтение свойства у undefined |
| ReferenceError | Имени не существует ни в одной области видимости | опечатка в имени переменной |
| RangeError | Тип верный, но значение вне допустимого диапазона | toFixed(200), бесконечная рекурсия |
| SyntaxError | Текст не удаётся разобрать как код или как JSON | JSON.parse со сломанной строкой |
Проще всего запомнить границу между двумя самыми частыми: ReferenceError — «такого имени нет», TypeError — «имя есть, но с его значением так нельзя».
Этот вид выпадает из общего ряда. Остальные ошибки случаются во время работы программы, а синтаксическая — до неё: движок сначала разбирает весь файл целиком. Если в нём забыта скобка, не выполнится ни одна строка — включая тот самый try, которым вы надеялись ошибку поймать.
Поэтому SyntaxError, который действительно ловят в приложениях, почти всегда прилетает из JSON.parse: там разбирается чужой текст, и происходит это уже во время работы программы.
Похожесть JSON на объектный литерал обманчива. Хвостовая запятая, одинарные кавычки, комментарий, undefined — всё это язык переживёт, а разбор JSON остановит.
Поле stack отвечает на вопрос «как мы сюда попали». Это обычный текст: в большинстве браузеров первой идёт строка с видом и сообщением, а дальше — список функций. Сверху та, внутри которой ошибка возникла, под ней — та, что её вызвала, и так вниз до верхнего уровня программы.
Читайте стек сверху вниз и ищите первую строку со своим файлом. Если верхние кадры принадлежат библиотеке, ошибка почти наверняка не в ней: это вы передали туда неподходящие данные, и первая ваша строка в списке и есть место, откуда стоит начинать разбор.
stack не описан в стандарте — формат у каждого движка свой. Читать глазами можно, разбирать регулярками не стоит.
Сообщения браузеров кажутся набором английских слов, но на деле их немного и они повторяются. Несколько формул стоит выучить сразу — они закрывают большую часть повседневных поломок.
| Текст сообщения | Что произошло | Вид |
|---|---|---|
| x is not a function | Вызвали то, что функцией не является: опечатка в имени метода или у этого типа его нет | TypeError |
| Cannot read properties of undefined | Слева от точки оказался undefined: объект не пришёл, ключ назван иначе, ответ ещё не загрузился | TypeError |
| x is not defined | Переменной с таким именем нет: опечатка, забытое объявление или другой файл | ReferenceError |
| Assignment to constant variable | Попытка присвоить заново тому, что объявлено через const | TypeError |
| Maximum call stack size exceeded | Рекурсия без условия выхода | RangeError |
Главный навык — смотреть не на то, что справа от точки, а на то, что слева. Сообщение «нельзя прочитать свойство items» жалуется вовсе не на items: виновато выражение перед ним, которое оказалось undefined.
Когда ошибку бросаете вы, берите не голый Error, а вид, который точнее описывает беду: TypeError — пришли данные не того типа, RangeError — тип верный, но значение вне допустимых границ. Тогда тот, кто ловит, сможет развести случаи по err.name или через instanceof, не разбирая текст сообщения.
Если встроенных видов не хватает, наследуйтесь от Error и задайте своё name. Такая ошибка остаётся обычной ошибкой — её ловит тот же catch, у неё есть stack, — но её легко отличить от чужой. Это вся механика, которая нужна здесь; следующая статья, «Свои классы ошибок», добавляет к ней cause, доменные поля и общего предка на семейство.
- Ошибка — это объект с полями name, message и stack; программу прерывает не создание объекта, а throw.
- TypeError — значение не того типа, ReferenceError — имени не существует, RangeError — значение вне границ, SyntaxError — текст не разбирается.
- Синтаксическую ошибку в собственном файле поймать нельзя: код не запускается вовсе. Ловят обычно ту, что пришла из JSON.parse.
- Стек читают сверху вниз: наверху место броска, ниже — цепочка вызовов; формат текста зависит от браузера.
- Сообщение про нечитаемое свойство указывает на то, что слева от точки; свои ошибки бросайте подходящим конструктором.
1/3Переменная counter нигде не объявлена. Какой будет вид ошибки у console.log(counter)?
2/3Что окажется в err.name, если поймать ошибку от JSON.parse("{name: Аня}")?
3/3Где в стеке вызовов искать место, в котором ошибка возникла?
Определить вид ошибки
Напишите функцию errorName(fn): она вызывает переданную функцию и возвращает name пойманной ошибки, а если ошибки не было — строку "ok".
Вся суть — в порядке строк внутри try: return "ok" должен стоять после вызова, иначе он выполнится раньше, чем ошибка успеет случиться.
Возвращать именно err.name, а не err.message, здесь важно: имя вида одинаково во всех браузерах, а текст сообщения у каждого движка свой.
Бросить подходящий вид
Функция getItem(arr, index) возвращает элемент массива. Если первый аргумент не массив — бросьте TypeError, если индекс выходит за границы — RangeError. Сообщения придумайте свои.
Проверки идут по порядку: сначала тип, потом диапазон — проверять границы у не-массива бессмысленно.
Строка тоже индексируется, поэтому без явной проверки Array.isArray вызов getItem("строка", 0) молча вернул бы букву вместо ошибки.