Неважно, насколько мы хороши в программировании, иногда наши скрипты содержат ошибки. Они могут возникать из-за наших промахов, неожиданного ввода пользователя, неправильного ответа сервера и по тысяче других причин.
Обычно скрипт в случае ошибки «падает» (сразу же останавливается), с выводом ошибки в консоль.
Но есть синтаксическая конструкция try..catch
, которая позволяет «ловить» ошибки и вместо падения делать что-то более осмысленное.
Синтаксис «try…catch»
Конструкция try..catch
состоит из двух основных блоков: try
, и затем catch
:
try {
// код...
} catch (err) {
// обработка ошибки
}
Работает она так:
- Сначала выполняется код внутри блока
try {...}
. - Если в нём нет ошибок, то блок
catch(err)
игнорируется: выполнение доходит до концаtry
и потом далее, полностью пропускаяcatch
. - Если же в нём возникает ошибка, то выполнение
try
прерывается, и поток управления переходит в началоcatch(err)
. Переменнаяerr
(можно использовать любое имя) содержит объект ошибки с подробной информацией о произошедшем.
Давайте рассмотрим примеры.
-
Пример без ошибок: выведет
alert
(1)
и(2)
:try { alert('Начало блока try'); // (1) <-- // ...код без ошибок alert('Конец блока try'); // (2) <-- } catch(err) { alert('Catch игнорируется, так как нет ошибок'); // (3) }
-
Пример с ошибками: выведет
(1)
и(3)
:try { alert('Начало блока try'); // (1) <-- lalala; // ошибка, переменная не определена! alert('Конец блока try (никогда не выполнится)'); // (2) } catch(err) { alert(`Возникла ошибка!`); // (3) <-- }
try..catch
работает только для ошибок, возникающих во время выполнения кода
Чтобы try..catch
работал, код должен быть выполнимым. Другими словами, это должен быть корректный JavaScript-код.
Он не сработает, если код синтаксически неверен, например, содержит несовпадающее количество фигурных скобок:
try {
{{{{{{{{{{{{
} catch(e) {
alert("Движок не может понять этот код, он некорректен");
}
JavaScript-движок сначала читает код, а затем исполняет его. Ошибки, которые возникают во время фазы чтения, называются ошибками парсинга. Их нельзя обработать (изнутри этого кода), потому что движок не понимает код.
Таким образом, try..catch
может обрабатывать только ошибки, которые возникают в корректном коде. Такие ошибки называют «ошибками во время выполнения», а иногда «исключениями».
try..catch
работает синхронно
Исключение, которое произойдёт в коде, запланированном «на будущее», например в setTimeout
, try..catch
не поймает:
try {
setTimeout(function() {
noSuchVariable; // скрипт упадёт тут
}, 1000);
} catch (e) {
alert( "не сработает" );
}
Это потому, что функция выполняется позже, когда движок уже покинул конструкцию try..catch
.
Чтобы поймать исключение внутри запланированной функции, try..catch
должен находиться внутри самой этой функции:
setTimeout(function() {
try {
noSuchVariable; // try..catch обрабатывает ошибку!
} catch {
alert( "ошибка поймана!" );
}
}, 1000);
Объект ошибки
Когда возникает ошибка, JavaScript генерирует объект, содержащий её детали. Затем этот объект передаётся как аргумент в блок catch
:
try {
// ...
} catch(err) { // <-- объект ошибки, можно использовать другое название вместо err
// ...
}
Для всех встроенных ошибок этот объект имеет два основных свойства:
name
- Имя ошибки. Например, для неопределённой переменной это
"ReferenceError"
. message
- Текстовое сообщение о деталях ошибки.
В большинстве окружений доступны и другие, нестандартные свойства. Одно из самых широко используемых и поддерживаемых – это:
stack
- Текущий стек вызова: строка, содержащая информацию о последовательности вложенных вызовов, которые привели к ошибке. Используется в целях отладки.
try {
lalala; // ошибка, переменная не определена!
} catch(err) {
alert(err.name); // ReferenceError
alert(err.message); // lalala is not defined
alert(err.stack); // ReferenceError: lalala is not defined at (...стек вызовов)
// Можем также просто вывести ошибку целиком
// Ошибка приводится к строке вида "name: message"
alert(err); // ReferenceError: lalala is not defined
}
Блок «catch» без переменной
Эта возможность была добавлена в язык недавно.
В старых браузерах может понадобиться полифил.
Если нам не нужны детали ошибки, в catch
можно её пропустить:
try {
// ...
} catch { // <-- без (err)
// ...
}
Использование «try…catch»
Давайте рассмотрим реальные случаи использования try..catch
.
Как мы уже знаем, JavaScript поддерживает метод JSON.parse(str) для чтения JSON.
Обычно он используется для декодирования данных, полученных по сети, от сервера или из другого источника.
Мы получаем их и вызываем JSON.parse
вот так:
let json = '{"name":"John", "age": 30}'; // данные с сервера
let user = JSON.parse(json); // преобразовали текстовое представление в JS-объект
// теперь user - объект со свойствами из строки
alert( user.name ); // John
alert( user.age ); // 30
Вы можете найти более детальную информацию о JSON в главе Формат JSON, метод toJSON.
Если json
некорректен, JSON.parse
генерирует ошибку, то есть скрипт «падает».
Устроит ли нас такое поведение? Конечно нет!
Получается, что если вдруг что-то не так с данными, то посетитель никогда (если, конечно, не откроет консоль) об этом не узнает. А люди очень не любят, когда что-то «просто падает» без всякого сообщения об ошибке.
Давайте используем try..catch
для обработки ошибки:
let json = "{ некорректный JSON }";
try {
let user = JSON.parse(json); // <-- тут возникает ошибка...
alert( user.name ); // не сработает
} catch (e) {
// ...выполнение прыгает сюда
alert( "Извините, в данных ошибка, мы попробуем получить их ещё раз." );
alert( e.name );
alert( e.message );
}
Генерация собственных ошибок
Что если json
синтаксически корректен, но не содержит необходимого свойства name
?
let json = '{ "age": 30 }'; // данные неполны
try {
let user = JSON.parse(json); // <-- выполнится без ошибок
alert( user.name ); // нет свойства name!
} catch (e) {
alert( "не выполнится" );
}
Здесь JSON.parse
выполнится без ошибок, но на самом деле отсутствие свойства name
для нас ошибка.
Для того, чтобы унифицировать обработку ошибок, мы воспользуемся оператором throw
.
Оператор «throw»
Оператор throw
генерирует ошибку.
Технически в качестве объекта ошибки можно передать что угодно. Это может быть даже примитив, число или строка, но всё же лучше, чтобы это был объект, желательно со свойствами name
и message
(для совместимости со встроенными ошибками).
В JavaScript есть множество встроенных конструкторов для стандартных ошибок: Error
, SyntaxError
, ReferenceError
, TypeError
и другие. Можно использовать и их для создания объектов ошибки.
let error = new Error(message);
// или
let error = new SyntaxError(message);
let error = new ReferenceError(message);
// ...
Для встроенных ошибок (не для любых объектов, только для ошибок), свойство name
– это в точности имя конструктора. А свойство message
берётся из аргумента.
let error = new Error(" Ого, ошибка! o_O");
alert(error.name); // Error
alert(error.message); // Ого, ошибка! o_O
Давайте посмотрим, какую ошибку генерирует JSON.parse
:
try {
JSON.parse("{ bad json o_O }");
} catch(e) {
alert(e.name); // SyntaxError
alert(e.message); // Unexpected token b in JSON at position 2
}
Как мы видим, это SyntaxError
.
В нашем случае отсутствие свойства name
– это ошибка, ведь пользователи должны иметь имена.
let json = '{ "age": 30 }'; // данные неполны
try {
let user = JSON.parse(json); // <-- выполнится без ошибок
if (!user.name) {
throw new SyntaxError("Данные неполны: нет имени"); // (*)
}
alert( user.name );
} catch(e) {
alert( "JSON Error: " + e.message ); // JSON Error: Данные неполны: нет имени
}
В строке (*)
оператор throw
генерирует ошибку SyntaxError
с сообщением message
. Точно такого же вида, как генерирует сам JavaScript. Выполнение блока try
немедленно останавливается, и поток управления прыгает в catch
.
Теперь блок catch
становится единственным местом для обработки всех ошибок: и для JSON.parse
и для других случаев.
Проброс исключения
let json = '{ "age": 30 }'; // данные неполны
try {
user = JSON.parse(json); // <-- забыл добавить "let" перед user
// ...
} catch(err) {
alert("JSON Error: " + err); // JSON Error: ReferenceError: user is not defined
// (не JSON ошибка на самом деле)
}
Конечно, возможно все! Программисты совершают ошибки. Даже в утилитах с открытым исходным кодом, используемых миллионами людей на протяжении десятилетий – вдруг может быть обнаружена ошибка, которая приводит к ужасным взломам.
В нашем случае try..catch
предназначен для выявления ошибок, связанных с некорректными данными. Но по своей природе catch
получает все свои ошибки из try
. Здесь он получает неожиданную ошибку, но всё также показывает то же самое сообщение "JSON Error"
. Это неправильно и затрудняет отладку кода.
К счастью, мы можем выяснить, какую ошибку мы получили, например, по её свойству name
:
try {
user = { /*...*/ };
} catch(e) {
alert(e.name); // "ReferenceError" из-за неопределённой переменной
}
Есть простое правило:
Блок catch
должен обрабатывать только те ошибки, которые ему известны, и «пробрасывать» все остальные.
Техника «проброс исключения» выглядит так:
- Блок
catch
получает все ошибки. - В блоке
catch(err) {...}
мы анализируем объект ошибкиerr
. - Если мы не знаем как её обработать, тогда делаем
throw err
.
В коде ниже мы используем проброс исключения, catch
обрабатывает только SyntaxError
:
let json = '{ "age": 30 }'; // данные неполны
try {
let user = JSON.parse(json);
if (!user.name) {
throw new SyntaxError("Данные неполны: нет имени");
}
blabla(); // неожиданная ошибка
alert( user.name );
} catch(e) {
if (e.name == "SyntaxError") {
alert( "JSON Error: " + e.message );
} else {
throw e; // проброс (*)
}
}
Ошибка в строке (*)
из блока catch
«выпадает наружу» и может быть поймана другой внешней конструкцией try..catch
(если есть), или «убьёт» скрипт.
Таким образом, блок catch
фактически обрабатывает только те ошибки, с которыми он знает, как справляться, и пропускает остальные.
Пример ниже демонстрирует, как такие ошибки могут быть пойманы с помощью ещё одного уровня try..catch
:
function readData() {
let json = '{ "age": 30 }';
try {
// ...
blabla(); // ошибка!
} catch (e) {
// ...
if (e.name != 'SyntaxError') {
throw e; // проброс исключения (не знаю как это обработать)
}
}
}
try {
readData();
} catch (e) {
alert( "Внешний catch поймал: " + e ); // поймал!
}
Здесь readData
знает только, как обработать SyntaxError
, тогда как внешний блок try..catch
знает, как обработать всё.
Try…catch…finally
Подождите, это ещё не всё.
Конструкция try..catch
может содержать ещё одну секцию: finally
.
Если секция есть, то она выполняется в любом случае:
- после
try
, если не было ошибок, - после
catch
, если ошибки были.
Расширенный синтаксис выглядит следующим образом:
try {
... пробуем выполнить код...
} catch(e) {
... обрабатываем ошибки ...
} finally {
... выполняем всегда ...
}
Попробуйте запустить такой код:
try {
alert( 'try' );
if (confirm('Сгенерировать ошибку?')) BAD_CODE();
} catch (e) {
alert( 'catch' );
} finally {
alert( 'finally' );
}
У кода есть два пути выполнения:
- Если вы ответите на вопрос «Сгенерировать ошибку?» утвердительно, то
try -> catch -> finally
. - Если ответите отрицательно, то
try -> finally
.
Секцию finally
часто используют, когда мы начали что-то делать и хотим завершить это вне зависимости от того, будет ошибка или нет.
Например, мы хотим измерить время, которое занимает функция чисел Фибоначчи fib(n)
. Естественно, мы можем начать измерения до того, как функция начнёт выполняться и закончить после. Но что делать, если при вызове функции возникла ошибка? В частности, реализация fib(n)
в коде ниже возвращает ошибку для отрицательных и для нецелых чисел.
Секция finally
отлично подходит для завершения измерений несмотря ни на что.
Здесь finally
гарантирует, что время будет измерено корректно в обеих ситуациях – и в случае успешного завершения fib
и в случае ошибки:
let num = +prompt("Введите положительное целое число?", 35)
let diff, result;
function fib(n) {
if (n < 0 || Math.trunc(n) != n) {
throw new Error("Должно быть целое неотрицательное число");
}
return n <= 1 ? n : fib(n - 1) + fib(n - 2);
}
let start = Date.now();
try {
result = fib(num);
} catch (e) {
result = 0;
} finally {
diff = Date.now() - start;
}
alert(result || "возникла ошибка");
alert( `Выполнение заняло ${diff}ms` );
Вы можете это проверить, запустив этот код и введя 35
в prompt
– код завершится нормально, finally
выполнится после try
. А затем введите -1
– незамедлительно произойдёт ошибка, выполнение займёт 0ms
. Оба измерения выполняются корректно.
Другими словами, неважно как завершилась функция: через return
или throw
. Секция finally
срабатывает в обоих случаях.
Переменные внутри try..catch..finally
локальны
Обратите внимание, что переменные result
и diff
в коде выше объявлены до try..catch
.
Если переменную объявить в блоке, например, в try
, то она не будет доступна после него.
finally
и return
Блок finally
срабатывает при любом выходе из try..catch
, в том числе и return
.
В примере ниже из try
происходит return
, но finally
получает управление до того, как контроль возвращается во внешний код.
function func() {
try {
return 1;
} catch (e) {
/* ... */
} finally {
alert( 'finally' );
}
}
alert( func() ); // сначала срабатывает alert из finally, а затем этот код
Конструкция try..finally
без секции catch
также полезна. Мы применяем её, когда не хотим здесь обрабатывать ошибки (пусть выпадут), но хотим быть уверены, что начатые процессы завершились.
function func() {
// начать делать что-то, что требует завершения (например, измерения)
try {
// ...
} finally {
// завершить это, даже если все упадёт
}
}
В приведённом выше коде ошибка всегда выпадает наружу, потому что тут нет блока catch
. Но finally
отрабатывает до того, как поток управления выйдет из функции.
Глобальный catch
Зависит от окружения
Информация из данной секции не является частью языка JavaScript.
Давайте представим, что произошла фатальная ошибка (программная или что-то ещё ужасное) снаружи try..catch
, и скрипт упал.
Существует ли способ отреагировать на такие ситуации? Мы можем захотеть залогировать ошибку, показать что-то пользователю (обычно они не видят сообщение об ошибке) и т.д.
Такого способа нет в спецификации, но обычно окружения предоставляют его, потому что это весьма полезно. Например, в Node.js для этого есть process.on("uncaughtException")
. А в браузере мы можем присвоить функцию специальному свойству window.onerror, которая будет вызвана в случае необработанной ошибки.
window.onerror = function(message, url, line, col, error) {
// ...
};
message
- Сообщение об ошибке.
url
- URL скрипта, в котором произошла ошибка.
-
line
,col
- Номера строки и столбца, в которых произошла ошибка.
error
- Объект ошибки.
Роль глобального обработчика window.onerror
обычно заключается не в восстановлении выполнения скрипта – это скорее всего невозможно в случае программной ошибки, а в отправке сообщения об ошибке разработчикам.
Существуют также веб-сервисы, которые предоставляют логирование ошибок для таких случаев, такие как https://errorception.com или http://www.muscula.com.
Они работают так:
- Мы регистрируемся в сервисе и получаем небольшой JS-скрипт (или URL скрипта) от них для вставки на страницы.
- Этот JS-скрипт ставит свою функцию
window.onerror
. - Когда возникает ошибка, она выполняется и отправляет сетевой запрос с информацией о ней в сервис.
- Мы можем войти в веб-интерфейс сервиса и увидеть ошибки.
Итого
Конструкция try..catch
позволяет обрабатывать ошибки во время исполнения кода. Она позволяет запустить код и перехватить ошибки, которые могут в нём возникнуть.
try {
// исполняем код
} catch(err) {
// если случилась ошибка, прыгаем сюда
// err - это объект ошибки
} finally {
// выполняется всегда после try/catch
}
Секций catch
или finally
может не быть, то есть более короткие конструкции try..catch
и try..finally
также корректны.
Объекты ошибок содержат следующие свойства:
-
message
– понятное человеку сообщение. -
name
– строка с именем ошибки (имя конструктора ошибки). -
stack
(нестандартное, но хорошо поддерживается) – стек на момент ошибки.
Мы можем также генерировать собственные ошибки, используя оператор throw
. Аргументом throw
может быть что угодно, но обычно это объект ошибки, наследуемый от встроенного класса Error
. Подробнее о расширении ошибок см. в следующей главе.
Проброс исключения – это очень важный приём обработки ошибок: блок catch
обычно ожидает и знает, как обработать определённый тип ошибок, поэтому он должен пробрасывать дальше ошибки, о которых он не знает.
Даже если у нас нет try..catch
, большинство сред позволяют настроить «глобальный» обработчик ошибок, чтобы ловить ошибки, которые «выпадают наружу». В браузере это window.onerror
.
Как бы мы хорошо ни программировали, в коде бывают ошибки. Или, как их иначе называют, «исключительные ситуации» (исключения).
Обычно скрипт при ошибке, как говорят, «падает», с выводом ошибки в консоль.
Но бывают случаи, когда нам хотелось бы как-то контролировать ситуацию, чтобы скрипт не просто «упал», а сделал что-то разумное.
Для этого в JavaScript есть замечательная конструкция try..catch
.
Конструкция try…catch
Конструкция try..catch
состоит из двух основных блоков: try
, и затем catch
:
try {
// код ...
} catch (err) {
// обработка ошибки
}
Работает она так:
-
Выполняется код внутри блока
try
. -
Если в нём ошибок нет, то блок
catch(err)
игнорируется, то есть выполнение доходит до концаtry
и потом прыгает черезcatch
. -
Если в нём возникнет ошибка, то выполнение
try
на ней прерывается, и управление прыгает в начало блокаcatch(err)
.При этом переменная
err
(можно выбрать и другое название) будет содержать объект ошибки с подробной информацией о произошедшем.
Таким образом, при ошибке в try
скрипт не «падает», и мы получаем возможность обработать ошибку внутри catch
.
Посмотрим это на примерах.
-
Пример без ошибок: при запуске сработают
alert
(1)
и(2)
:try { alert('Начало блока try'); // (1) <-- // .. код без ошибок alert('Конец блока try'); // (2) <-- } catch(e) { alert('Блок catch не получит управление, так как нет ошибок'); // (3) } alert("Потом код продолжит выполнение...");
-
Пример с ошибкой: при запуске сработают
(1)
и(3)
:try { alert('Начало блока try'); // (1) <-- lalala; // ошибка, переменная не определена! alert('Конец блока try'); // (2) } catch(e) { alert('Ошибка ' + e.name + ":" + e.message + "\n" + e.stack); // (3) <-- } alert("Потом код продолжит выполнение...");
Если грубо нарушена структура кода, например не закрыта фигурная скобка или где-то стоит лишняя запятая, то никакой try..catch
здесь не поможет. Такие ошибки называются синтаксическими, интерпретатор не может понять такой код.
Здесь же мы рассматриваем ошибки семантические, то есть происходящие в корректном коде, в процессе выполнения.
Ошибку, которая произойдёт в коде, запланированном «на будущее», например в setTimeout
, try..catch
не поймает:
try {
setTimeout(function() {
throw new Error(); // вылетит в консоль
}, 1000);
} catch (e) {
alert( "не сработает" );
}
На момент запуска функции, назначенной через setTimeout
, этот код уже завершится, интерпретатор выйдет из блока try..catch
.
Чтобы поймать ошибку внутри функции из setTimeout
, и try..catch
должен быть в той же функции.
Объект ошибки
В примере выше мы видим объект ошибки. У него есть три основных свойства:
name
- Тип ошибки. Например, при обращении к несуществующей переменной:
"ReferenceError"
. message
- Текстовое сообщение о деталях ошибки.
stack
- Везде, кроме IE8-, есть также свойство
stack
, которое содержит строку с информацией о последовательности вызовов, которая привела к ошибке.
В зависимости от браузера у него могут быть и дополнительные свойства, см. Error в MDN и Error в MSDN.
Пример использования
В JavaScript есть встроенный метод JSON.parse(str), который используется для чтения JavaScript-объектов (и не только) из строки.
Обычно он используется для того, чтобы обрабатывать данные, полученные по сети, с сервера или из другого источника.
Мы получаем их и вызываем метод JSON.parse
, вот так:
var data = '{"name":"Вася", "age": 30}'; // строка с данными, полученная с сервера
var user = JSON.parse(data); // преобразовали строку в объект
// теперь user -- это JS-объект с данными из строки
alert( user.name ); // Вася
alert( user.age ); // 30
Более детально формат JSON разобран в главе Формат JSON, метод toJSON.
В случае, если данные некорректны, JSON.parse
генерирует ошибку, то есть скрипт «упадёт».
Устроит ли нас такое поведение? Конечно нет!
Получается, что если вдруг что-то не так с данными, то посетитель никогда (если, конечно, не откроет консоль) об этом не узнает.
А люди очень-очень не любят, когда что-то «просто падает», без всякого объявления об ошибке.
Бывают ситуации, когда без try..catch
не обойтись, это – одна из таких.
Используем try..catch
, чтобы обработать некорректный ответ:
var data = "Has Error"; // в данных ошибка
try {
var user = JSON.parse(data); // <-- ошибка при выполнении
alert( user.name ); // не сработает
} catch (e) {
// ...выполнится catch
alert( "Извините, в данных ошибка, мы попробуем получить их ещё раз" );
alert( e.name );
alert( e.message );
}
Генерация своих ошибок
var data = '{ "age": 30 }'; // данные неполны
try {
var user = JSON.parse(data); // <-- выполнится без ошибок
alert( user.name ); // undefined
} catch (e) {
// не выполнится
alert( "Извините, в данных ошибка" );
}
Вызов JSON.parse
выполнится без ошибок, но ошибка в данных есть. И, так как свойство name
обязательно должно быть, то для нас это такие же некорректные данные, как и "Has Error"
.
Для того, чтобы унифицировать и объединить обработку ошибок парсинга и ошибок в структуре, мы воспользуемся оператором throw
.
Оператор throw
Оператор throw
генерирует ошибку.
Синтаксис: throw <объект ошибки>
.
Технически в качестве объекта ошибки можно передать что угодно, это может быть даже не объект, а число или строка, но всё же лучше, чтобы это был объект, желательно – совместимый со стандартным, то есть чтобы у него были как минимум свойства name
и message
.
В качестве конструктора ошибок можно использовать встроенный конструктор: new Error(message)
или любой другой.
В JavaScript встроен ряд конструкторов для стандартных ошибок: SyntaxError
, ReferenceError
, RangeError
и некоторые другие. Можно использовать и их, но только чтобы не было путаницы.
В данном случае мы используем конструктор new SyntaxError(message)
. Он создаёт ошибку того же типа, что и JSON.parse
.
var data = '{ "age": 30 }'; // данные неполны
try {
var user = JSON.parse(data); // <-- выполнится без ошибок
if (!user.name) {
throw new SyntaxError("Данные некорректны");
}
alert( user.name );
} catch (e) {
alert( "Извините, в данных ошибка" );
}
Получилось, что блок catch
– единое место для обработки ошибок во всех случаях: когда ошибка выявляется при JSON.parse
или позже.
Проброс исключения
В коде выше мы предусмотрели обработку ошибок, которые возникают при некорректных данных. Но может ли быть так, что возникнет какая-то другая ошибка?
Конечно, может! Код – это вообще мешок с ошибками, бывает даже так, что библиотеку выкладывают в открытый доступ, она там 10 лет лежит, её смотрят миллионы людей и на 11-й год находятся опаснейшие ошибки. Такова жизнь, таковы люди.
Блок catch
в нашем примере предназначен для обработки ошибок, возникающих при некорректных данных. Если же в него попала какая-то другая ошибка, то вывод сообщения о «некорректных данных» будет дезинформацией посетителя.
Ошибку, о которой catch
не знает, он не должен обрабатывать.
Такая техника называется «проброс исключения»: в catch(e)
мы анализируем объект ошибки, и если он нам не подходит, то делаем throw e
.
При этом ошибка «выпадает» из try..catch
наружу. Далее она может быть поймана либо внешним блоком try..catch
(если есть), либо «повалит» скрипт.
В примере ниже catch
обрабатывает только ошибки SyntaxError
, а остальные – выбрасывает дальше:
var data = '{ "name": "Вася", "age": 30 }'; // данные корректны
try {
var user = JSON.parse(data);
if (!user.name) {
throw new SyntaxError("Ошибка в данных");
}
blabla(); // произошла непредусмотренная ошибка
alert( user.name );
} catch (e) {
if (e.name == "SyntaxError") {
alert( "Извините, в данных ошибка" );
} else {
throw e;
}
}
Заметим, что ошибка, которая возникла внутри блока catch
, «выпадает» наружу, как если бы была в обычном коде.
В следующем примере такие ошибки обрабатываются ещё одним, «более внешним» try..catch
:
function readData() {
var data = '{ "name": "Вася", "age": 30 }';
try {
// ...
blabla(); // ошибка!
} catch (e) {
// ...
if (e.name != 'SyntaxError') {
throw e; // пробрасываем
}
}
}
try {
readData();
} catch (e) {
alert( "Поймал во внешнем catch: " + e ); // ловим
}
В примере выше try..catch
внутри readData
умеет обрабатывать только SyntaxError
, а внешний – все ошибки.
Без внешнего проброшенная ошибка «вывалилась» бы в консоль с остановкой скрипта.
Оборачивание исключений
И, для полноты картины – последняя, самая продвинутая техника по работе с ошибками. Она, впрочем, является стандартной практикой во многих объектно-ориентированных языках.
Цель функции readData
в примере выше – прочитать данные. При чтении могут возникать разные ошибки, не только SyntaxError
, но и, возможно, к примеру URIError
(неправильное применение функций работы с URI) да и другие.
Код, который вызвал readData
, хотел бы иметь либо результат, либо информацию об ошибке.
При этом очень важным является вопрос: обязан ли этот внешний код знать о всевозможных типах ошибок, которые могут возникать при чтении данных, и уметь перехватывать их?
Обычно внешний код хотел бы работать «на уровень выше», и получать либо результат, либо «ошибку чтения данных», при этом какая именно ошибка произошла – ему неважно. Ну, или, если будет важно, то хотелось бы иметь возможность это узнать, но обычно не требуется.
Это важнейший общий подход к проектированию – каждый участок функциональности должен получать информацию на том уровне, который ей необходим.
Мы его видим везде в грамотно построенном коде, но не всегда отдаём себе в этом отчёт.
В данном случае, если при чтении данных происходит ошибка, то мы будем генерировать её в виде объекта ReadError
, с соответствующим сообщением. А «исходную» ошибку на всякий случай тоже сохраним, присвоим в свойство cause
(англ. – причина).
Выглядит это так:
function ReadError(message, cause) {
this.message = message;
this.cause = cause;
this.name = 'ReadError';
this.stack = cause.stack;
}
function readData() {
var data = '{ bad data }';
try {
// ...
JSON.parse(data);
// ...
} catch (e) {
// ...
if (e.name == 'URIError') {
throw new ReadError("Ошибка в URI", e);
} else if (e.name == 'SyntaxError') {
throw new ReadError("Синтаксическая ошибка в данных", e);
} else {
throw e; // пробрасываем
}
}
}
try {
readData();
} catch (e) {
if (e.name == 'ReadError') {
alert( e.message );
alert( e.cause ); // оригинальная ошибка-причина
} else {
throw e;
}
}
Этот подход называют «оборачиванием» исключения, поскольку мы берём ошибки «более низкого уровня» и «заворачиваем» их в ReadError
, которая соответствует текущей задаче.
Секция finally
Конструкция try..catch
может содержать ещё один блок: finally
.
Выглядит этот расширенный синтаксис так:
try {
.. пробуем выполнить код ..
} catch(e) {
.. перехватываем исключение ..
} finally {
.. выполняем всегда ..
}
Секция finally
не обязательна, но если она есть, то она выполняется всегда:
- после блока
try
, если ошибок не было, - после
catch
, если они были.
Попробуйте запустить такой код?
try {
alert( 'try' );
if (confirm('Сгенерировать ошибку?')) BAD_CODE();
} catch (e) {
alert( 'catch' );
} finally {
alert( 'finally' );
}
У него два варианта работы:
- Если вы ответите на вопрос «сгенерировать ошибку?» утвердительно, то
try -> catch -> finally
. - Если ответите отрицательно, то
try -> finally
.
Секцию finally
используют, чтобы завершить начатые операции при любом варианте развития событий.
Например, мы хотим подсчитать время на выполнение функции sum(n)
, которая должна возвратить сумму чисел от 1
до n
и работает рекурсивно:
function sum(n) {
return n ? (n + sum(n - 1)) : 0;
}
var n = +prompt('Введите n?', 100);
var start = new Date();
try {
var result = sum(n);
} catch (e) {
result = 0;
} finally {
var diff = new Date() - start;
}
alert( result ? result : 'была ошибка' );
alert( "Выполнение заняло " + diff );
Здесь секция finally
гарантирует, что время будет подсчитано в любых ситуациях: при ошибке в sum
или без неё.
Вы можете проверить это, запустив код с указанием n=100
– будет без ошибки, finally
выполнится после try
, а затем с n=100000
– будет ошибка из-за слишком глубокой рекурсии, управление прыгнет в finally
после catch
.
Блок finally
срабатывает при любом выходе из try..catch
, в том числе и return
.
В примере ниже из try
происходит return
, но finally
получает управление до того, как контроль возвращается во внешний код.
function func() {
try {
// сразу вернуть значение
return 1;
} catch (e) {
/* ... */
} finally {
alert( 'finally' );
}
}
alert( func() ); // сначала finally, потом 1
Если внутри try
были начаты какие-то процессы, которые нужно завершить по окончании работы, то в finally
это обязательно будет сделано.
Кстати, для таких случаев иногда используют try..finally
вообще без catch
:
function func() {
try {
return 1;
} finally {
alert( 'Вызов завершён' );
}
}
alert( func() ); // сначала finally, потом 1
В примере выше try..finally
вообще не обрабатывает ошибки. Задача в другом: выполнить код при любом выходе из try
– с ошибкой ли, без ошибок или через return
.
Последняя надежда
Допустим, ошибка произошла вне блока try..catch
или выпала из try..catch
наружу, во внешний код. Скрипт упал.
Можно ли как-то узнать о том, что произошло? Да, конечно.
В браузере существует специальное свойство window.onerror
, если в него записать функцию, то она выполнится и получит в аргументах сообщение ошибки, текущий URL и номер строки, откуда «выпала» ошибка.
Необходимо лишь позаботиться, чтобы функция была назначена заранее.
Как правило, роль window.onerror
заключается не в том, чтобы оживить скрипт – скорее всего, это уже невозможно, а в том, чтобы отослать сообщение об ошибке на сервер, где разработчики о ней узнают.
Существуют даже специальные веб-сервисы, которые предоставляют скрипты для отлова и аналитики таких ошибок, например: https://errorception.com/ или https://www.muscula.com/.
Итого
Обработка ошибок – большая и важная тема.
В JavaScript для этого предусмотрены:
-
Конструкция
try..catch..finally
– она позволяет обработать произвольные ошибки в блоке кода.Это удобно в тех случаях, когда проще сделать действие и потом разбираться с результатом, чем долго и нудно проверять, не упадёт ли чего.
Кроме того, иногда проверить просто невозможно, например
JSON.parse(str)
не позволяет «проверить» формат строки перед разбором. В этом случае блокtry..catch
необходим.Полный вид конструкции:
try { .. пробуем выполнить код .. } catch(e) { .. перехватываем исключение .. } finally { .. выполняем всегда .. }
Возможны также варианты
try..catch
илиtry..finally
. -
Оператор
throw err
генерирует свою ошибку, в качествеerr
рекомендуется использовать объекты, совместимые с встроенным типом Error, содержащие свойстваmessage
иname
.
Кроме того, мы рассмотрели некоторые важные приёмы:
-
Проброс исключения –
catch(err)
должен обрабатывать только те ошибки, которые мы рассчитываем в нём увидеть, остальные – пробрасывать дальше черезthrow err
.Определить, нужная ли это ошибка, можно, например, по свойству
name
. -
Оборачивание исключений – функция, в процессе работы которой возможны различные виды ошибок, может «обернуть их» в одну общую ошибку, специфичную для её задачи, и уже её пробросить дальше. Чтобы при необходимости можно было подробно определить, что произошло, исходную ошибку обычно присваивают в свойство этой, общей. Обычно это нужно для логирования.
-
В
window.onerror
можно присвоить функцию, которая выполнится при любой «выпавшей» из скрипта ошибке. Как правило, это используют в информационных целях, например отправляют информацию об ошибке на специальный сервис.
В этом руководстве мы погрузимся в обработку ошибок JavaScript, чтобы вы могли выбрасывать исключения, обнаруживать и обрабатывать собственные ошибки.
Опытные разработчики ожидают неожиданного. Если что-то может пойти не так, так оно и будет — обычно в тот момент, когда первый пользователь получает доступ к вашему новому приложению.
Некоторых ошибок веб-приложений можно избежать, например:
- Хороший редактор или линтер может отлавливать синтаксические ошибки.
- Хорошая валидация может выявить ошибки пользовательского ввода.
- Надёжные процессы тестирования могут обнаруживать логические ошибки.
И всё же ошибки остаются. Браузеры могут сбоить или не поддерживать API, который мы используем. Серверы могут дать сбой или слишком долго отвечать. Сетевое соединение может выйти из строя или стать ненадёжным. Проблемы могут быть временными, но мы не может программировать такие проблемы. Однако мы можем предвидеть проблемы, принимать меры по их устранению и повышать отказоустойчивость нашего приложения.
Отображение сообщения об ошибке — крайняя мера
В идеале пользователи никогда не должны видеть сообщения об ошибке.
Мы можем игнорировать незначительные проблемы, такие как невозможность загрузки декоративного изображения. Мы могли бы решить более серьёзные проблемы, такие как сбои сохранения данных Ajax локально и загружая их позже. Ошибка становиться необходимой только тогда, когда пользователь рискует потерять данные, предполагая, что он может что-то с этим сделать.
Поэтому необходимо выявлять ошибки по мере их возникновения и определять наилучшие действия. Вызов и перехват ошибок в приложении JavaScript поначалу может быть пугающим, но, возможно, это проще, чем вы ожидаете.
Как JavaScript обрабатывает ошибки
Когда оператор JavaScript приводит к ошибке, говорят, что он генерирует (выбрасывает) исключение. JavaScript создаёт и выбрасывает объект Error
, описывающий ошибку. Мы можем увидеть это в действии на CodePen. Если установить в десятичные разряды (decimal places) отрицательное число, мы увидим сообщение об ошибке в консоли внизу. (Обратите внимание, что мы не встраиваем CodePen в это руководство, потому что нужно иметь возможно видеть вывод консоли, чтобы этот пример имел смысл)
Результат не обновиться, и мы увидим сообщение RangeError
в консоли. Следующая функция выдаёт ошибку, когда dp
имеет отрицательно значение:
// division calculation
v1 v2 dp
v1 v2dp
После выдачи ошибки интерпретатор JavaScript проверяет наличие кода обработки исключений. В функции Division()
ничего нет, поэтому она проверяет вызывающую функцию:
// show result of division
resultvalue
num1value
num2value
dpvalue
Интерпретатор повторяет процесс для каждой функции в стеке вызовов, пока не произойдёт одно из следующих событий:
- Он находит обработчик исключений.
- Он достигает верхнего уровня кода (что приводит к завершению программы и отображению ошибки в консоли, как показано в примере на CodePen выше).
Перехват исключений
// division calculation
v1 v2 dp
v1 v2dp
e
console
Теперь result
показывает ERROR
. Консоль показывает имя ошибки и сообщение, но это выводится оператором console.log
и не завершает работу программы.
v1 v2 dp
v1 v2dp
e
console
В консоль выведется done
, независимо от того, успешно ли выполнено вычисление или возникла ошибка. Блок finally
обычно выполняет действия, которые в противном случае нам пришлось бы повторять как в блоке try
, так и в блоке catch
. Например, отмену вызова API или закрытие соединения с базой данных.
Для блока try
требуется либо блок catch
, либо блок finally
, либо и то и другое. Обратите внимание, что когда блок finally
содержит оператор return
, это значение становится возвращаемым значением для всей функции; другие операторы в блоках try
или catch
игнорируются.
Вложенные обработчики исключений
Что произойдёт, если мы добавим обработчик исключений к вызывающей функции showResult()
?
// show result of division
resultvalue
num1value
num2value
dpvalue
e
resultvalue
Тем не менее мы могли бы программно генерировать новый объект Error
в divide()
и при желании передать исходную ошибку в свойстве cause
второго аргумента:
v1 v2 dp
v1 v2dp
e
e
Это вызовет блок catch
в вызывающей функции:
// show result of division
e
console emessage
console ecausename
resultvalue
Стандартные типы ошибок JavaScript
Когда возникает исключение, JavaScript создаёт и выдаёт объект, описывающий ошибку, используя один из следующих типов.
SyntaxError
Ошибка, возникающая из-за синтаксически недопустимого кода, такого как отсутствующая скобка:
condition
console'condition is true'
Примечание: такие языки, как C++ и Java, сообщают об ошибках синтаксиса во время компиляции. JavaScript — интерпретируемый язык, поэтому синтаксические ошибки не выявляются до тех пор, пока код не запустится. Любой хороший редактор кода или линтер могут обнаружить синтаксические ошибки до того, как мы попытаемся запустить код.
ReferenceError
Ошибка при доступе к несуществующей переменной:
value
Опять, хороший редактор кода или линтер могут обнаружить эту проблему.
TypeError
Ошибка возникает, когда значение не соответствует ожидаемому типу, например, при вызове несуществующего метода объекта:
obj
obj
RangeError
Ошибка возникает, когда значение не входит в набор или диапазон допустимых значений. Используемый выше метод toFixed()
генерирует эту ошибку, потому что он ожидает значение от 0 до 100:
n
console n
URIError
Ошибка выдаваемая функциями обработки URI, такими как encodeURI()
и decodeURI()
, при обнаружении неправильных URI:
u
EvalError
Ошибка возникающая при передаче строки, содержащей не валидный JavaScript код, в функцию eval()
:
Примечание: пожалуйста, не используйте
eval()
! Выполнение произвольного кода, содержащегося в строке, возможно, созданной на основе пользовательского ввода, слишком опасно!
AggregateError
Ошибка возникает, когда несколько ошибок объединены в одну ошибку. Обычно возникает при вызове такой операции, как Promise.all()
, которая возвращает результаты нескольких промисов.
InternalError
Нестандартная ошибка (только в Firefox) возникает при возникновении внутренней ошибки движка JavaScript. Обычно это результат того, что что-то занимает слишком много памяти, например, большой массив или слишком много рекурсии
.
Error
Генерация/выбрасывание собственных исключений
Мы можем использовать throw
для генерации/выбрасывания собственных исключений, когда возникает ошибка — или должна произойти. Например:
- нашей функции не передаются валидные параметры
- ajax-запрос не возвращает ожидаемые данные
- обновление DOM завершается ошибкой, поскольку узел не существует
Оператор throw
фактически принимает любое значение или объект. Например:
'A simple error string'
Исключения генерируются для каждой функции в стеке вызовов до тех пор, пока они не будут перехвачены обработчиком исключений (catch
). Однако на практике мы хотим создать и сгенерировать объект Error
, чтобы он действовал идентично стандартным ошибкам, выдаваемым JavaScript.
Можно создать общий объект Error
, передав необязательное сообщение конструктору:
'An error has occurred'
Так же Error
можно использовать как функцию, без new
. Она возвращает объект Error
, идентичный приведённому выше:
'An error has occurred'
При желании можно передать имя файла и номер строки в качестве второго и третьего параметров:
'An error has occurred'
В этом редко возникает необходимость, так как по умолчанию они относятся к файлу и строке, где мы вызвали объект Error
. (Также их сложно поддерживать, поскольку наши файлы меняются!)
Мы можем определить общие объекты Error
, но по возможности следует использовать стандартный тип Error
. Например:
'Decimal places must be 0 or greater'
Все объекты Error
имеют следующие свойства, которые можно проверить в блоке catch
:
-
.name
: имя типа ошибки, напримерError
илиRangeError
. -
.message
: сообщение об ошибке.
В Firefox поддерживаются следующие нестандартные свойства:
-
.fileName
: файл, в котором произошла ошибка. -
.lineNumber
: номер строки, в которой произошла ошибка. -
.columnNumber
: номер столбца, в котором произошла ошибка. -
.stack
: трассировка стека со списком вызовов функций, сделанных до возникновения ошибки.
Мы можем изменить функцию divide()
так, чтобы она вызывала ошибку RangeError
, когда количество знаков после запятой не является числом, меньше нуля и больше восьми:
// division calculation
v1 v2 dp
dp dp dp
'Decimal places must be between 0 and 8'
v1 v2dp
Точно так же мы могли бы выдать Error
или TypeError
, когда значения делимого не является числом, чтобы предотвратить результат NaN
:
v1
'Dividend must be a number'
Также можно обрабатывать делитель, который не является числом или равен нулю. JavaScript возвращает Infinity
при делении на ноль, но это может запутать пользователя. Вместо того чтобы вызывать общую ошибку, мы могли бы создать собственный тип ошибки DivByZeroError
:
// new DivByZeroError Error type
message
name
Затем вызывать/выбрасывать его подобным образом:
v2 v2
'Divisor must be a non-zero number'
// show result of division
resultvalue
num1value
num2value
dpvalue
errmsgtextContent
e
resultvalue
errmsgtextContent emessage
console ename
Попробуйте ввести недопустимые нечисловые, нулевые и отрицательные значения в демонстрации на CodePen.
Окончательная версия функции divide()
проверяет все входящие значения и при необходимости выдаёт соответствующую ошибку:
// division calculation
v1 v2 dp
v1
'Dividend must be a number'
v2 v2
'Divisor must be a non-zero number'
dp dp dp
'Decimal places must be between 0 and 8'
v1 v2dp
Ошибки асинхронной функции
delay
'I am never caught!'
delay
e
console'This will never run'
Соглашение, принятое в большинстве фреймворков и серверных сред выполнения, таких как Node.js, заключается в том, чтобы возвращать ошибку в качестве первого параметра функции обратного вызова. Это не приведёт к возникновению исключения, хотя при необходимости мы можем вручную сгенерировать ошибку:
delay callback
'This is an error message'
delay
e
Ошибки на основе промисов
Обратные вызовы могут стать громоздкими, поэтому при написании асинхронного кода предпочтительнее использовать промисы. При возникновении ошибки метод reject()
промиса может вернуть новый объект Error
или любое другое значение:
delay
delay delay
delay
Примечание: функции должны быть либо 100% синхронными, либо 100% асинхронными. Вот почему необходимо проверять значение
delay
внутри возвращаемого промиса. Если бы мы проверили значениеdelay
и выдали ошибку перед возвратом промиса, функция стала бы синхронной при возникновении ошибки.
Метод Promise.catch()
выполняется при передаче недопустимого параметра delay
и получает возвращённый объект Error
:
// invalid delay value passed
console res
console emessage
console
Следующая (вызываемая немедленно) асинхронная функция функционально идентична цепочке промисов выше:
console
e
console emessage
console
Исключительная обработка исключения
Выбрасывать объекты Error
и обрабатывать исключения в JavaScript легко:
'I am an error!'
e
console
Создание отказоустойчивого приложения, адекватно реагирующего на ошибки и облегчающее жизнь пользователя, является сложным испытанием. Всегда ожидайте неожиданного.