Как бы мы ни были хороши в программировании, иногда в наших скриптах есть ошибки. Они могут возникнуть из-за наших ошибок, неожиданного ввода пользователя, ошибочного ответа сервера и по тысяче других причин.
Обычно скрипт «умирает» (сразу останавливается) в случае ошибки, выводя его на консоль.
Но есть синтаксическая конструкция try...catch
, которая позволяет нам «перехватывать» ошибки, чтобы скрипт мог вместо того, чтобы умереть, сделать что-то более разумное.
Конструкция try...catch
состоит из двух основных блоков: try
и then catch
:
пытаться { // код... } поймать (ошибиться) { // обработка ошибок }
Это работает следующим образом:
Сначала выполняется код try {...}
.
Если ошибок не было, то catch (err)
игнорируется: выполнение доходит до конца try
и продолжается, пропуская catch
.
Если возникает ошибка, выполнение try
останавливается и управление переходит к началу catch (err)
. Переменная err
(мы можем использовать для нее любое имя) будет содержать объект ошибки с подробностями о том, что произошло.
Таким образом, ошибка внутри блока try {...}
не убивает скрипт — у нас есть возможность обработать ее в catch
.
Давайте посмотрим на несколько примеров.
Безошибочный пример: показывает alert
(1)
и (2)
:
пытаться { alert('Начало пробного запуска'); // (1) <-- // ...здесь ошибок нет alert('Окончание попытки'); // (2) <-- } поймать (ошибиться) { alert('Catch игнорируется, так как ошибок нет'); // (3) }
Пример с ошибкой: показаны (1)
и (3)
:
пытаться { alert('Начало пробного запуска'); // (1) <-- лалала; // ошибка, переменная не определена! alert('Конец попытки (никогда не достигнут)'); // (2) } поймать (ошибиться) { alert(`Произошла ошибка!`); // (3) <-- }
try...catch
работает только для ошибок времени выполнения
Чтобы try...catch
работал, код должен быть работоспособным. Другими словами, это должен быть действительный JavaScript.
Это не сработает, если код синтаксически неправильный, например, в нем есть несовпадающие фигурные скобки:
пытаться { {{{{{{{{{{{{{ } поймать (ошибиться) { alert("Машина не может понять этот код, он недействителен"); }
Движок JavaScript сначала считывает код, а затем запускает его. Ошибки, возникающие на этапе чтения, называются ошибками «время анализа» и не подлежат восстановлению (изнутри этого кода). Это потому, что движок не может понять код.
Итак, try...catch
может обрабатывать только ошибки, возникающие в допустимом коде. Такие ошибки называются «ошибками времени выполнения» или иногда «исключениями».
try...catch
работает синхронно
Если исключение происходит в «запланированном» коде, например, в setTimeout
, то try...catch
его не перехватит:
пытаться { setTimeout(функция() { нетТакаяПеременная; // скрипт умрет здесь }, 1000); } поймать (ошибиться) { Предупреждение("не будет работать"); }
Это связано с тем, что сама функция выполняется позже, когда движок уже покинул конструкцию try...catch
.
Чтобы перехватить исключение внутри запланированной функции, try...catch
должна находиться внутри этой функции:
setTimeout(функция() { пытаться { нетТакаяПеременная; // попробуем...catch обработает ошибку! } ловить { alert("Здесь обнаружена ошибка!"); } }, 1000);
При возникновении ошибки JavaScript генерирует объект, содержащий подробную информацию о ней. Затем объект передается в качестве аргумента функции catch
:
пытаться { // ... } catch (err) { // <-- "объект ошибки", вместо err можно использовать другое слово // ... }
Для всех встроенных ошибок объект ошибки имеет два основных свойства:
name
Имя ошибки. Например, для неопределенной переменной "ReferenceError"
.
message
Текстовое сообщение о деталях ошибки.
В большинстве сред доступны и другие нестандартные свойства. Одним из наиболее широко используемых и поддерживаемых является:
stack
Текущий стек вызовов: строка с информацией о последовательности вложенных вызовов, приведших к ошибке. Используется в целях отладки.
Например:
пытаться { лалала; // ошибка, переменная не определена! } поймать (ошибиться) { оповещение(ошибка.имя); // Ошибка ссылки оповещение (ошибка.сообщение); // лалала не определена оповещение(err.stack); // Ошибка ссылки: lalala не определена в (...стек вызовов) // Также может показать ошибку целиком // Ошибка преобразуется в строку как "имя: сообщение" предупреждение (ошибка); // Ошибка ссылки: лалала не определена }
Недавнее дополнение
Это недавнее дополнение к языку. Старым браузерам могут потребоваться полифилы.
Если нам не нужны подробности ошибки, catch
может их опустить:
пытаться { // ... } catch { // <-- без (ошибка) // ... }
Давайте рассмотрим реальный вариант использования try...catch
.
Как мы уже знаем, JavaScript поддерживает метод JSON.parse(str) для чтения значений в кодировке JSON.
Обычно его используют для декодирования данных, полученных по сети, с сервера или другого источника.
Мы получаем его и вызываем JSON.parse
следующим образом:
let json = '{"name":"Джон", "возраст": 30}'; // данные с сервера пусть пользователь = JSON.parse(json); // преобразуем текстовое представление в объект JS // теперь пользователь — это объект со свойствами из строки оповещение(имя_пользователя); // Джон оповещение(пользователь.возраст); // 30
Более подробную информацию о JSON можно найти в разделе «Методы JSON», глава «JSON».
Если json
имеет неверный формат, JSON.parse
генерирует ошибку, поэтому скрипт «умирает».
Должны ли мы быть удовлетворены этим? Конечно, нет!
Таким образом, если с данными что-то не так, посетитель никогда об этом не узнает (если не откроет консоль разработчика). А людям очень не нравится, когда что-то «просто умирает» без какого-либо сообщения об ошибке.
Давайте воспользуемся try...catch
для обработки ошибки:
let json = "{плохой json}"; пытаться { пусть пользователь = JSON.parse(json); // <-- при возникновении ошибки... оповещение(имя_пользователя); // не работает } поймать (ошибиться) { // ...выполнение переходит сюда alert("Приносим извинения, данные содержат ошибки, мы попробуем запросить их еще раз."); предупреждение(ошибка.имя); предупреждение (ошибка.сообщение); }
Здесь мы используем блок catch
только для того, чтобы показать сообщение, но мы можем сделать гораздо больше: отправить новый сетевой запрос, предложить посетителю альтернативу, отправить информацию об ошибке в средство регистрации и т. д. Все гораздо лучше, чем просто умереть.
Что, если json
синтаксически правильный, но не имеет обязательного свойства name
?
Так:
let json = '{ "age": 30 }'; // неполные данные пытаться { пусть пользователь = JSON.parse(json); // <-- ошибок нет оповещение(имя_пользователя); // нет имени! } поймать (ошибиться) { alert("не выполняется"); }
Здесь JSON.parse
работает нормально, но отсутствие name
для нас фактически является ошибкой.
Чтобы унифицировать обработку ошибок, мы воспользуемся оператором throw
.
Оператор throw
генерирует ошибку.
Синтаксис:
бросить <объект ошибки>
Технически мы можем использовать что угодно в качестве объекта ошибки. Это может быть даже примитив, например число или строка, но лучше использовать объекты, желательно со свойствами name
и message
(чтобы обеспечить некоторую совместимость со встроенными ошибками).
В JavaScript имеется множество встроенных конструкторов для стандартных ошибок: Error
, SyntaxError
, ReferenceError
, TypeError
и другие. Мы также можем использовать их для создания объектов ошибок.
Их синтаксис:
пусть ошибка = новая ошибка (сообщение); // или пусть ошибка = новый SyntaxError(сообщение); пусть ошибка = новая ReferenceError (сообщение); // ...
Для встроенных ошибок (не для каких-либо объектов, а только для ошибок) свойство name
— это именно имя конструктора. И message
берется из аргумента.
Например:
let error = new Error("Вещи случаются o_O"); оповещение(ошибка.имя); // Ошибка предупреждение (ошибка.сообщение); // Что-то случается o_O
Давайте посмотрим, какую ошибку генерирует JSON.parse
:
пытаться { JSON.parse("{плохой json o_O }"); } поймать (ошибиться) { оповещение(ошибка.имя); // Синтаксическая ошибка оповещение (ошибка.сообщение); // Неожиданный токен b в JSON на позиции 2 }
Как мы видим, это SyntaxError
.
А в нашем случае отсутствие name
является ошибкой, так как у пользователя должно быть name
.
Итак, давайте бросим это:
let json = '{ "age": 30 }'; // неполные данные пытаться { пусть пользователь = JSON.parse(json); // <-- ошибок нет если (!user.name) { throw new SyntaxError("Неполные данные: нет имени"); // (*) } оповещение(имя_пользователя); } поймать (ошибиться) { alert( "Ошибка JSON: " + err.message); // Ошибка JSON: неполные данные: нет имени }
В строке (*)
оператор throw
генерирует SyntaxError
с данным message
так же, как JavaScript генерирует его сам. Выполнение try
немедленно прекращается, и поток управления переходит в catch
.
Теперь catch
стал единым местом для всей обработки ошибок: как для JSON.parse
, так и для других случаев.
В приведенном выше примере мы используем try...catch
для обработки неверных данных. Но возможно ли, что в блоке try {...}
произойдет еще одна неожиданная ошибка ? Например, ошибка программирования (переменная не определена) или что-то еще, а не только эти «неправильные данные».
Например:
let json = '{ "age": 30 }'; // неполные данные пытаться { пользователь = JSON.parse(json); // <-- забыл поставить "let" перед пользователем // ... } поймать (ошибиться) { alert("Ошибка JSON: " + err); // Ошибка JSON: ReferenceError: пользователь не определен // (на самом деле нет ошибки JSON) }
Конечно, все возможно! Программисты делают ошибки. Даже в утилитах с открытым исходным кодом, используемых миллионами на протяжении десятилетий – внезапно может обнаружиться ошибка, приводящая к ужасным взломам.
В нашем случае try...catch
ставится для перехвата ошибок «неверных данных». Но по своей природе catch
получает все ошибки от try
. Здесь возникает неожиданная ошибка, но по-прежнему отображается то же сообщение "JSON Error"
. Это неправильно, а также усложняет отладку кода.
Чтобы избежать таких проблем, мы можем использовать технику «переброса». Правило простое:
Catch должен обрабатывать только те ошибки, которые ему известны, и «перебрасывать» все остальные.
Более подробно технику «переброса» можно объяснить так:
Catch получает все ошибки.
В блоке catch (err) {...}
мы анализируем объект ошибки err
.
Если мы не знаем, как с этим справиться, мы throw err
.
Обычно мы можем проверить тип ошибки с помощью оператора instanceof
:
пытаться { пользователь = { /*...*/ }; } поймать (ошибиться) { если (ошибка экземпляра ReferenceError) { Оповещение('Ошибка ссылки'); // «ReferenceError» для доступа к неопределенной переменной } }
Мы также можем получить имя класса ошибки из свойства err.name
. Он есть у всех родных ошибок. Другой вариант — прочитать err.constructor.name
.
В приведенном ниже коде мы используем повторное выбрасывание, чтобы catch
обрабатывал только SyntaxError
:
let json = '{ "age": 30 }'; // неполные данные пытаться { пусть пользователь = JSON.parse(json); если (!user.name) { throw new SyntaxError("Неполные данные: нет имени"); } блабла(); // неожиданная ошибка оповещение(имя_пользователя); } поймать (ошибиться) { если (ошибка экземпляра SyntaxError) { alert( "Ошибка JSON: " + err.message); } еще { выбросить ошибку; // повторно выбрасываем (*) } }
Ошибка, выбрасываемая в строке (*)
из внутреннего блока catch
«выпадает» из try...catch
и может быть либо перехвачена внешней конструкцией try...catch
(если она существует), либо она убивает скрипт.
Таким образом, блок catch
фактически обрабатывает только те ошибки, с которыми он знает, как справиться, и «пропускает» все остальные.
В приведенном ниже примере показано, как такие ошибки могут быть обнаружены с помощью еще одного уровня try...catch
:
функция readData() { let json = '{ "age": 30 }'; пытаться { // ... блабла(); // ошибка! } поймать (ошибиться) { // ... if (!(err instanceof SyntaxError)) { выбросить ошибку; // повторно выбрасываем (не знаю, как с этим бороться) } } } пытаться { читатьДанные(); } поймать (ошибиться) { alert("Получен внешний улов: " + err); // поймал! }
Здесь readData
знает только, как обрабатывать SyntaxError
, а внешний try...catch
знает, как обрабатывать все.
Подождите, это еще не все.
Конструкция try...catch
может иметь еще одно предложение кода: finally
.
Если он существует, он запускается во всех случаях:
после try
, если не было ошибок,
после catch
, если были ошибки.
Расширенный синтаксис выглядит следующим образом:
пытаться { ... попробуйте выполнить код... } поймать (ошибиться) { ...обрабатывать ошибки... } окончательно { ... выполнять всегда... }
Попробуйте запустить этот код:
пытаться { Предупреждение('попробуй'); if (confirm('Допустили ошибку?')) BAD_CODE(); } поймать (ошибиться) { предупреждение('поймать'); } окончательно { Предупреждение('Наконец-то'); }
Код имеет два способа исполнения:
Если вы ответите «Да» на «Допустить ошибку?», то try -> catch -> finally
.
Если вы скажете «Нет», то try -> finally
.
Предложение finally
часто используется, когда мы начинаем что-то делать и хотим завершить это в любом случае.
Например, мы хотим измерить время, которое занимает функция чисел Фибоначчи fib(n)
. Естественно, мы можем начать измерение до его запуска и закончить позже. Но что, если во время вызова функции произойдет ошибка? В частности, реализация fib(n)
в приведенном ниже коде возвращает ошибку для отрицательных или нецелых чисел.
Раздел finally
— отличное место для завершения измерений, несмотря ни на что.
Вот finally
гарантии, что время будет измерено правильно в обеих ситуациях — в случае успешного выполнения fib
и в случае ошибки в нем:
let num = +prompt("Введите положительное целое число?", 35) пусть разница, результат; функция фиб(п) { if (n <0 || Math.trunc(n) != n) { throw new Error("Не должно быть отрицательным, а также целым числом."); } вернуть n <= 1? n : фиб(n - 1) + фиб(n - 2); } пусть старт = Date.now(); пытаться { результат = Фиб (число); } поймать (ошибиться) { результат = 0; } окончательно { разница = Date.now() - начало; } alert(результат || «произошла ошибка»); alert(`выполнение заняло ${diff}ms`);
Вы можете проверить, запустив код, введя 35
в prompt
– он выполняется нормально, finally
, после try
. А затем введите -1
— немедленно произойдет ошибка, и выполнение займет 0ms
. Оба измерения выполнены правильно.
Другими словами, функция может завершиться с помощью return
или throw
, это не имеет значения. Предложение finally
выполняется в обоих случаях.
Переменные являются локальными внутри try...catch...finally
Обратите внимание, что переменные result
и diff
в приведенном выше коде объявляются перед try...catch
.
В противном случае, если бы мы объявили блок let
in try
, он был бы виден только внутри него.
finally
и return
Предложение finally
работает для любого выхода из try...catch
. Это включает в себя явный return
.
В примере ниже в try
есть return
. В этом случае, finally
выполняется непосредственно перед возвратом управления во внешний код.
функция func() { пытаться { возврат 1; } поймать (ошибиться) { /* ... */ } окончательно { Предупреждение('Наконец-то'); } } предупреждение(функция()); // сначала срабатывает оповещение отfinally, а потом это
try...finally
Конструкция try...finally
без предложения catch
также полезна. Мы применяем его, когда не хотим здесь обрабатывать ошибки (пусть они проваливаются), но хотим быть уверены, что процессы, которые мы начали, завершатся.
функция func() { // начинаем делать что-то, что требует завершения (например, измерения) пытаться { // ... } окончательно { // завершить это дело, даже если все умрут } }
В приведенном выше коде всегда вываливается ошибка внутри try
, потому что нет catch
. Но finally
работает до того, как поток выполнения покинет функцию.
С учетом окружающей среды
Информация из этого раздела не является частью основного JavaScript.
Давайте представим, что у нас произошла фатальная ошибка вне try...catch
, и скрипт перестал работать. Типа ошибки программирования или еще какой-то ужасной вещи.
Есть ли способ реагировать на подобные происшествия? Мы можем захотеть зарегистрировать ошибку, показать что-то пользователю (обычно они не видят сообщений об ошибках) и т. д.
В спецификации его нет, но среда обычно его предоставляет, потому что это действительно полезно. Например, в Node.js для этого есть process.on("uncaughtException")
. А в браузере мы можем назначить функцию специальному свойству window.onerror, которая будет запускаться в случае необнаруженной ошибки.
Синтаксис:
window.onerror = функция (сообщение, URL, строка, столбец, ошибка) { // ... };
message
Сообщение об ошибке.
url
URL-адрес сценария, в котором произошла ошибка.
line
, col
Номера строк и столбцов, в которых произошла ошибка.
error
Объект ошибки.
Например:
<скрипт> window.onerror = функция (сообщение, URL, строка, столбец, ошибка) { alert(`${message}n At ${line}:${col} of ${url}`); }; функция readData() { плохаяФунк(); // Упс, что-то пошло не так! } читатьДанные(); </скрипт>
Роль глобального обработчика window.onerror
обычно заключается не в восстановлении выполнения скрипта — это, вероятно, невозможно в случае ошибок программирования, а в отправке сообщения об ошибке разработчикам.
Существуют также веб-сервисы, обеспечивающие регистрацию ошибок в таких случаях, например https://errorception.com или https://www.muscula.com.
Они работают следующим образом:
Регистрируемся на сервисе и получаем от них кусок JS (или URL скрипта) для вставки на страницы.
Этот JS-скрипт устанавливает специальную функцию window.onerror
.
При возникновении ошибки он отправляет сетевой запрос об этом сервису.
Мы можем войти в веб-интерфейс сервиса и увидеть ошибки.
Конструкция try...catch
позволяет обрабатывать ошибки во время выполнения. Он буквально позволяет «попробовать» запустить код и «отловить» ошибки, которые могут в нем возникнуть.
Синтаксис:
пытаться { // запускаем этот код } поймать (ошибиться) { // если произошла ошибка, то переходим сюда // err — объект ошибки } окончательно { // делаем в любом случае после try/catch }
Раздела catch
может отсутствовать или, finally
, не быть, поэтому более короткие конструкции try...catch
и try...finally
также допустимы.
Объекты ошибок имеют следующие свойства:
message
– удобочитаемое сообщение об ошибке.
name
– строка с именем ошибки (имя конструктора ошибки).
stack
(нестандартный, но хорошо поддерживаемый) — стек на момент создания ошибки.
Если объект ошибки не нужен, мы можем опустить его, используя catch {
вместо catch (err) {
.
Мы также можем генерировать собственные ошибки с помощью оператора throw
. Технически аргумент throw
может быть любым, но обычно это объект ошибки, унаследованный от встроенного класса Error
. Подробнее о расширении ошибок в следующей главе.
Повторное выбрасывание является очень важным шаблоном обработки ошибок: блок catch
обычно ожидает и знает, как обработать конкретный тип ошибки, поэтому он должен повторно выдавать ошибки, о которых он не знает.
Даже если у нас нет try...catch
, большинство сред позволяют нам настроить «глобальный» обработчик ошибок для перехвата «выпадающих» ошибок. В браузере это window.onerror
.
важность: 5
Сравните два фрагмента кода.
Первый использует finally
для выполнения кода после try...catch
:
пытаться { работа работа } поймать (ошибиться) { обрабатывать ошибки } окончательно { навести порядок на рабочем месте }
Второй фрагмент выполняет очистку сразу после try...catch
:
пытаться { работа работа } поймать (ошибиться) { обрабатывать ошибки } навести порядок на рабочем месте
Очистка после работы обязательно нужна, неважно, была ошибка или нет.
Есть ли здесь преимущество в finally
или оба фрагмента кода равны? Если такое преимущество есть, то приведите пример, когда оно имеет значение.
Разница становится очевидной, если мы посмотрим на код внутри функции.
Поведение будет другим, если произойдет «выпрыгивание» из try...catch
.
Например, когда внутри try...catch
есть return
. finally
работает в случае любого выхода из try...catch
, даже через оператор return
: сразу после завершения try...catch
, но до того, как вызывающий код получит управление.
функция е() { пытаться { оповещение('старт'); вернуть «результат»; } поймать (ошибиться) { /// ... } окончательно { alert('очистка!'); } } е(); // очистка!
…Или когда происходит throw
, как здесь:
функция е() { пытаться { оповещение('старт'); throw new Error("ошибка"); } поймать (ошибиться) { // ... if("не могу обработать ошибку") { выбросить ошибку; } } окончательно { alert('очистка!') } } е(); // очистка!
finally
-то это гарантирует здесь чистоту. Если мы просто поместим код в конец f
, в таких ситуациях он не будет работать.