28 KiB
Перехват ошибок, "try..catch"
Как бы мы хорошо ни программировали, в коде бывают ошибки. Или, как их иначе называют, "исключительные ситуации" (исключения).
Обычно скрипт при ошибке, как говорят, "падает", с выводом ошибки в консоль.
Но бывают случаи, когда нам хотелось бы как-то контролировать ситуацию, чтобы скрипт не просто "упал", а сделал что-то разумное.
Для этого в JavaScript есть замечательная конструкция try..catch
.
[cut]
Конструкция try..catch
Конструкция try..catch
состоит из двух основных блоков: try
, и затем catch
:
try {
// код ...
} catch (err) {
// обработка ошибки
}
Работает она так:
- Выполняется код внутри блока `try`.
- Если в нём ошибок нет, то блок `catch(err)` игнорируется, то есть выполнение доходит до конца `try` и потом прыгает через `catch`.
- Если в нём возникнет ошибка, то выполнение `try` на ней прерывается, и управление прыгает в начало блока `catch(err)`.
При этом переменная
err
(можно выбрать и другое название) будет содержать объект ошибки с подробной информацией о произошедшем.
Таким образом, при ошибке в try
скрипт не "падает", и мы получаем возможность обработать ошибку внутри catch
.
Посмотрим это на примерах.
- Пример без ошибок: при запуске сработают `alert` `(1)` и `(2)`:
//+ run try { alert('Начало блока try'); // *!*(1) <--*/!* // .. код без ошибок alert('Конец блока try'); // *!*(2) <--*/!* } catch(e) { alert('Блок catch не получит управление, так как нет ошибок'); // (3) } alert("Потом код продолжит выполнение...");
- Пример с ошибкой: при запуске сработают `(1)` и `(3)`:
//+ run try { alert('Начало блока try'); // *!*(1) <--*/!* *!* lalala; // ошибка, переменная не определена! */!* alert('Конец блока try'); // (2) } catch(e) { alert('Ошибка ' + e.name + ":" + e.message + "\n" + e.stack); // *!*(3) <--*/!* } alert("Потом код продолжит выполнение...");
[warn header="try..catch
подразумевает, что код синтаксически верен"]
Если грубо нарушена структура кода, например не закрыта фигурная скобка или где-то стоит лишняя запятая, то никакой try..catch
здесь не поможет. Такие ошибки называются синтаксическими, интерпретатор не может понять такой код.
Здесь же мы рассматриваем ошибки семантические, то есть происходящие в корректном коде, в процессе выполнения. [/warn]
[warn header="try..catch
работает только в синхронном коде"]
Ошибку, которая произойдёт в коде, запланированном "на будущее", например, в setTimeout
, try..catch
не поймает:
//+ run
try {
setTimeout(function() {
throw new Error(); // вылетит в консоль
}, 1000);
} catch (e) {
alert( "не сработает" );
}
На момент запуска функции, назначенной через setTimeout
, этот код уже завершится, интерпретатор выйдет из блока try..catch
.
Чтобы поймать ошибку внутри функции из setTimeout
, и try..catch
должен быть в той же функции.
[/warn]
Объект ошибки
В примере выше мы видим объект ошибки. У него есть три основных свойства:
- `name`
- Тип ошибки. Например, при обращении к несуществующей переменной: `"ReferenceError"`.
- `message`
- Текстовое сообщение о деталях ошибки.
- `stack`
- Везде, кроме IE8-, есть также свойство `stack`, которое содержит строку с информацией о последовательности вызовов, которая привела к ошибке.
В зависимости от браузера, у него могут быть и дополнительные свойства, см. Error в MDN и Error в MSDN.
Пример использования
В JavaScript есть встроенный метод JSON.parse(str), который используется для чтения JavaScript-объектов (и не только) из строки.
Обычно он используется для того, чтобы обрабатывать данные, полученные по сети, с сервера или из другого источника.
Мы получаем их и вызываем метод JSON.parse
, вот так:
//+ run
var data = '{"name":"Вася", "age": 30}'; // строка с данными, полученная с сервера
var user = JSON.parse(data); // преобразовали строку в объект
// теперь user -- это JS-объект с данными из строки
alert( user.name ); // Вася
alert( user.age ); // 30
Более детально формат JSON разобран в главе .
В случае, если данные некорректны, JSON.parse
генерирует ошибку, то есть скрипт "упадёт".
Устроит ли нас такое поведение? Конечно нет!
Получается, что если вдруг что-то не так с данными, то посетитель никогда (если, конечно, не откроет консоль) об этом не узнает.
А люди очень-очень не любят, когда что-то "просто падает", без всякого объявления об ошибке.
Бывают ситуации, когда без try..catch
не обойтись, это -- одна из таких.
Используем try..catch
, чтобы обработать некорректный ответ:
//+ run
var data = "Has Error"; // в данных ошибка
try {
var user = JSON.parse(data); // <-- ошибка при выполнении
alert( user.name ); // не сработает
} catch (e) {
// ...выполнится catch
alert( "Извините, в данных ошибка, мы попробуем получить их ещё раз" );
alert( e.name );
alert( e.message );
}
Здесь в alert
только выводится сообщение, но область применения гораздо шире: можно повторять запрос, можно предлагать посетителю использовать альтернативный способ, можно отсылать информацию об ошибке на сервер... Свобода действий.
Генерация своих ошибок
Представим на минуту, что данные являются корректным JSON... Но в этом объекте нет нужного свойства name
:
//+ run
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
.
//+ run
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
, а остальные -- выбрасывает дальше:
//+ run
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
:
//+ run
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
(англ. -- причина).
Выглядит это так:
//+ run
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`, если они были.
Попробуйте запустить такой код?
//+ run
try {
alert( 'try' );
if (confirm('Сгенерировать ошибку?')) BAD_CODE();
} catch (e) {
alert( 'catch' );
} finally {
alert( 'finally' );
}
У него два варианта работы:
- Если вы ответите на вопрос "Сгенерировать ошибку?" утвердительно, то `try -> catch -> finally`.
- Если ответите отрицательно, то `try -> finally`.
Секцию finally
используют, чтобы завершить начатые операции при любом варианте развития событий.
Например, мы хотим подсчитать время на выполнение функции sum(n)
, которая должна возвратить сумму чисел от 1
до n
и работает рекурсивно:
//+ run
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
.
[smart header="finally
и return
"]
Блок finally
срабатывает при любом выходе из try..catch
, в том числе и return
.
В примере ниже, из try
происходит return
, но finally
получает управление до того, как контроль возвращается во внешний код.
//+ run
function func() {
try {
// сразу вернуть значение
return 1;
} catch (e) {
/* ... */
} finally {
*!*
alert( 'finally' );
*/!*
}
}
alert( func() ); // сначала finally, потом 1
Если внутри try
были начаты какие-то процессы, которые нужно завершить по окончании работы, во в finally
это обязательно будет сделано.
Кстати, для таких случаев иногда используют try..finally
вообще без catch
:
//+ run
function func() {
try {
return 1;
} finally {
alert( 'Вызов завершён' );
}
}
alert( func() ); // сначала finally, потом 1
В примере выше try..finally
вообще не обрабатывает ошибки. Задача в другом -- выполнить код при любом выходе из try
-- с ошибкой ли, без ошибок или через return
.
[/smart]
Последняя надежда: window.onerror
Допустим, ошибка произошла вне блока try..catch
или выпала из try..catch
наружу, во внешний код. Скрипт упал.
Можно ли как-то узнать о том, что произошло? Да, конечно.
В браузере существует специальное свойство window.onerror
, если в него записать функцию, то она выполнится и получит в аргументах сообщение ошибки, текущий URL и номер строки, откуда "выпала" ошибка.
Необходимо лишь позаботиться, чтобы функция была назначена заранее.
Например:
<!--+ run -->
<script>
*!*
window.onerror = function(message, url, lineNumber) {
alert("Поймана ошибка, выпавшая в глобальную область!\n" +
"Сообщение: " + message + "\n(" + url + ":" + lineNumber + ")");
};
*/!*
function readData() {
error(); // ой, что-то не так
}
readData();
</script>
Как правило, роль window.onerror
заключается в том, чтобы не оживить скрипт -- скорее всего, это уже невозможно, а в том, чтобы отослать сообщение об ошибке на сервер, где разработчики о ней узнают.
Существуют даже специальные веб-сервисы, которые предоставляют скрипты для отлова и аналитики таких ошибок, например: или .
Итого
Обработка ошибок -- большая и важная тема.
В JavaScript для этого предусмотрены:
- Конструкция `try..catch..finally` -- она позволяет обработать произвольные ошибки в блоке кода.
Это удобно в тех случаях, когда проще сделать действие и потом разбираться с результатом, чем долго и нудно проверять, не упадёт ли чего.
Кроме того, иногда проверить просто невозможно, например
JSON.parse(str)
не позволяет "проверить" формат строки перед разбором. В этом случае блокtry..catch
необходим.Полный вид конструкции:
*!*try*/!* { .. пробуем выполнить код .. } *!*catch*/!*(e) { .. перехватываем исключение .. } *!*finally*/!* { .. выполняем всегда .. }
Возможны также варианты
try..catch
илиtry..finally
. - Оператор `throw err` генерирует свою ошибку, в качестве `err` рекомендуется использовать объекты, совместимые с встроенным типом [Error](http://javascript.ru/Error), содержащие свойства `message` и `name`.
Кроме того, мы рассмотрели некоторые важные приёмы:
- Проброс исключения -- `catch(err)` должен обрабатывать только те ошибки, которые мы рассчитываем в нём увидеть, остальные -- пробрасывать дальше через `throw err`.
Определить, нужная ли это ошибка, можно, например, по свойству
name
. - Оборачивание исключений -- функция, в процессе работы которой возможны различные виды ошибок, может "обернуть их" в одну общую ошибку, специфичную для её задачи, и уже её пробросить дальше. Чтобы, при необходимости, можно было подробно определить, что произошло, исходную ошибку обычно присваивают в свойство этой, общей. Обычно это нужно для логирования.
- В `window.onerror` можно присвоить функцию, которая выполнится при любой "выпавшей" из скрипта ошибке. Как правило, это используют в информационных целях, например отправляют информацию об ошибке на специальный сервис.