590 lines
28 KiB
Markdown
590 lines
28 KiB
Markdown
# Перехват ошибок, "try..catch"
|
||
|
||
Как бы мы хорошо ни программировали, в коде бывают ошибки. Или, как их иначе называют, "исключительные ситуации" (исключения).
|
||
|
||
Обычно скрипт при ошибке, как говорят, "падает", с выводом ошибки в консоль.
|
||
|
||
Но бывают случаи, когда нам хотелось бы как-то контролировать ситуацию, чтобы скрипт не просто "упал", а сделал что-то разумное.
|
||
|
||
Для этого в JavaScript есть замечательная конструкция `try..catch`.
|
||
|
||
[cut]
|
||
|
||
## Конструкция try..catch
|
||
|
||
Конструкция `try..catch` состоит из двух основных блоков: `try`, и затем `catch`:
|
||
|
||
```js
|
||
try {
|
||
|
||
// код ...
|
||
|
||
} catch (err) {
|
||
|
||
// обработка ошибки
|
||
|
||
}
|
||
```
|
||
|
||
Работает она так:
|
||
<ol>
|
||
<li>Выполняется код внутри блока `try`.</li>
|
||
<li>Если в нём ошибок нет, то блок `catch(err)` игнорируется, то есть выполнение доходит до конца `try` и потом прыгает через `catch`.</li>
|
||
<li>Если в нём возникнет ошибка, то выполнение `try` на ней прерывается, и управление прыгает в начало блока `catch(err)`.
|
||
|
||
При этом переменная `err` (можно выбрать и другое название) будет содержать объект ошибки с подробной информацией о произошедшем.</li>
|
||
</ol>
|
||
|
||
**Таким образом, при ошибке в `try` скрипт не "падает", и мы получаем возможность обработать ошибку внутри `catch`.**
|
||
|
||
Посмотрим это на примерах.
|
||
|
||
<ul>
|
||
<li>Пример без ошибок: при запуске сработают `alert` `(1)` и `(2)`:
|
||
|
||
```js
|
||
//+ run
|
||
try {
|
||
|
||
alert('Начало блока try'); // *!*(1) <--*/!*
|
||
|
||
// .. код без ошибок
|
||
|
||
alert('Конец блока try'); // *!*(2) <--*/!*
|
||
|
||
} catch(e) {
|
||
|
||
alert('Блок catch не получит управление, так как нет ошибок'); // (3)
|
||
|
||
}
|
||
|
||
alert("Потом код продолжит выполнение...");
|
||
```
|
||
|
||
</li>
|
||
<li>Пример с ошибкой: при запуске сработают `(1)` и `(3)`:
|
||
|
||
```js
|
||
//+ run
|
||
try {
|
||
|
||
alert('Начало блока try'); // *!*(1) <--*/!*
|
||
|
||
*!*
|
||
lalala; // ошибка, переменная не определена!
|
||
*/!*
|
||
|
||
alert('Конец блока try'); // (2)
|
||
|
||
} catch(e) {
|
||
|
||
alert('Ошибка ' + e.name + ":" + e.message + "\n" + e.stack); // *!*(3) <--*/!*
|
||
|
||
}
|
||
|
||
alert("Потом код продолжит выполнение...");
|
||
```
|
||
|
||
</li>
|
||
</ul>
|
||
|
||
[warn header="`try..catch` подразумевает, что код синтаксически верен"]
|
||
Если грубо нарушена структура кода, например не закрыта фигурная скобка или где-то стоит лишняя запятая, то никакой `try..catch` здесь не поможет. Такие ошибки называются *синтаксическими*, интерпретатор не может понять такой код.
|
||
|
||
Здесь же мы рассматриваем ошибки *семантические*, то есть происходящие в корректном коде, в процессе выполнения.
|
||
[/warn]
|
||
|
||
|
||
[warn header="`try..catch` работает только в синхронном коде"]
|
||
Ошибку, которая произойдёт в коде, запланированном "на будущее", например, в `setTimeout`, `try..catch` не поймает:
|
||
|
||
```js
|
||
//+ run
|
||
try {
|
||
setTimeout(function() {
|
||
throw new Error(); // вылетит в консоль
|
||
}, 1000);
|
||
} catch (e) {
|
||
alert( "не сработает" );
|
||
}
|
||
```
|
||
|
||
На момент запуска функции, назначенной через `setTimeout`, этот код уже завершится, интерпретатор выйдет из блока `try..catch`.
|
||
|
||
Чтобы поймать ошибку внутри функции из `setTimeout`, и `try..catch` должен быть в той же функции.
|
||
[/warn]
|
||
|
||
|
||
## Объект ошибки
|
||
|
||
В примере выше мы видим объект ошибки. У него есть три основных свойства:
|
||
<dl>
|
||
<dt>`name`</dt>
|
||
<dd>Тип ошибки. Например, при обращении к несуществующей переменной: `"ReferenceError"`.</dd>
|
||
<dt>`message`</dt>
|
||
<dd>Текстовое сообщение о деталях ошибки.</dd>
|
||
<dt>`stack`</dt>
|
||
<dd>Везде, кроме IE8-, есть также свойство `stack`, которое содержит строку с информацией о последовательности вызовов, которая привела к ошибке.</dd>
|
||
</dl>
|
||
|
||
В зависимости от браузера, у него могут быть и дополнительные свойства, см. <a href="https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error">Error в MDN</a> и <a href="http://msdn.microsoft.com/en-us/library/dww52sbt%28v=vs.85%29.aspx">Error в MSDN</a>.
|
||
|
||
## Пример использования
|
||
|
||
В JavaScript есть встроенный метод [JSON.parse(str)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse), который используется для чтения JavaScript-объектов (и не только) из строки.
|
||
|
||
Обычно он используется для того, чтобы обрабатывать данные, полученные по сети, с сервера или из другого источника.
|
||
|
||
Мы получаем их и вызываем метод `JSON.parse`, вот так:
|
||
|
||
```js
|
||
//+ run
|
||
var data = '{"name":"Вася", "age": 30}'; // строка с данными, полученная с сервера
|
||
|
||
var user = JSON.parse(data); // преобразовали строку в объект
|
||
|
||
// теперь user -- это JS-объект с данными из строки
|
||
alert( user.name ); // Вася
|
||
alert( user.age ); // 30
|
||
```
|
||
|
||
Более детально формат JSON разобран в главе [](/json).
|
||
|
||
**В случае, если данные некорректны, `JSON.parse` генерирует ошибку, то есть скрипт "упадёт".**
|
||
|
||
Устроит ли нас такое поведение? Конечно нет!
|
||
|
||
Получается, что если вдруг что-то не так с данными, то посетитель никогда (если, конечно, не откроет консоль) об этом не узнает.
|
||
|
||
А люди очень-очень не любят, когда что-то "просто падает", без всякого объявления об ошибке.
|
||
|
||
**Бывают ситуации, когда без `try..catch` не обойтись, это -- одна из таких.**
|
||
|
||
Используем `try..catch`, чтобы обработать некорректный ответ:
|
||
|
||
```js
|
||
//+ 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`:
|
||
|
||
```js
|
||
//+ 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`.
|
||
|
||
```js
|
||
//+ 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`, а остальные -- выбрасывает дальше:
|
||
|
||
```js
|
||
//+ 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`:
|
||
|
||
```js
|
||
//+ 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` (англ. -- причина).
|
||
|
||
Выглядит это так:
|
||
```js
|
||
//+ 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`.
|
||
|
||
Выглядит этот расширенный синтаксис так:
|
||
|
||
```js
|
||
*!*try*/!* {
|
||
.. пробуем выполнить код ..
|
||
} *!*catch*/!*(e) {
|
||
.. перехватываем исключение ..
|
||
} *!*finally*/!* {
|
||
.. выполняем всегда ..
|
||
}
|
||
```
|
||
|
||
Секция `finally` не обязательна, но если она есть, то она выполняется всегда:
|
||
<ul>
|
||
<li>после блока `try`, если ошибок не было,</li>
|
||
<li>после `catch`, если они были.</li>
|
||
</ul>
|
||
|
||
Попробуйте запустить такой код?
|
||
|
||
```js
|
||
//+ run
|
||
try {
|
||
alert( 'try' );
|
||
if (confirm('Сгенерировать ошибку?')) BAD_CODE();
|
||
} catch (e) {
|
||
alert( 'catch' );
|
||
} finally {
|
||
alert( 'finally' );
|
||
}
|
||
```
|
||
|
||
У него два варианта работы:
|
||
<ol>
|
||
<li>Если вы ответите на вопрос "Сгенерировать ошибку?" утвердительно, то `try -> catch -> finally`.</li>
|
||
<li>Если ответите отрицательно, то `try -> finally`.
|
||
</ol>
|
||
|
||
**Секцию `finally` используют, чтобы завершить начатые операции при любом варианте развития событий.**
|
||
|
||
Например, мы хотим подсчитать время на выполнение функции `sum(n)`, которая должна возвратить сумму чисел от `1` до `n` и работает рекурсивно:
|
||
|
||
```js
|
||
//+ 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` получает управление до того, как контроль возвращается во внешний код.
|
||
|
||
```js
|
||
//+ run
|
||
function func() {
|
||
|
||
try {
|
||
// сразу вернуть значение
|
||
return 1;
|
||
|
||
} catch (e) {
|
||
/* ... */
|
||
} finally {
|
||
*!*
|
||
alert( 'finally' );
|
||
*/!*
|
||
}
|
||
}
|
||
|
||
alert( func() ); // сначала finally, потом 1
|
||
```
|
||
|
||
Если внутри `try` были начаты какие-то процессы, которые нужно завершить по окончании работы, во в `finally` это обязательно будет сделано.
|
||
|
||
Кстати, для таких случаев иногда используют `try..finally` вообще без `catch`:
|
||
|
||
```js
|
||
//+ 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 и номер строки, откуда "выпала" ошибка.
|
||
|
||
Необходимо лишь позаботиться, чтобы функция была назначена заранее.
|
||
|
||
Например:
|
||
|
||
```html
|
||
<!--+ run -->
|
||
<script>
|
||
*!*
|
||
window.onerror = function(message, url, lineNumber) {
|
||
alert("Поймана ошибка, выпавшая в глобальную область!\n" +
|
||
"Сообщение: " + message + "\n(" + url + ":" + lineNumber + ")");
|
||
};
|
||
*/!*
|
||
|
||
function readData() {
|
||
error(); // ой, что-то не так
|
||
}
|
||
|
||
readData();
|
||
</script>
|
||
```
|
||
|
||
Как правило, роль `window.onerror` заключается в том, чтобы не оживить скрипт -- скорее всего, это уже невозможно, а в том, чтобы отослать сообщение об ошибке на сервер, где разработчики о ней узнают.
|
||
|
||
Существуют даже специальные веб-сервисы, которые предоставляют скрипты для отлова и аналитики таких ошибок, например: [](https://errorception.com/) или [](http://www.muscula.com/).
|
||
|
||
|
||
## Итого
|
||
|
||
Обработка ошибок -- большая и важная тема.
|
||
|
||
В JavaScript для этого предусмотрены:
|
||
|
||
<ul>
|
||
<li>Конструкция `try..catch..finally` -- она позволяет обработать произвольные ошибки в блоке кода.
|
||
|
||
Это удобно в тех случаях, когда проще сделать действие и потом разбираться с результатом, чем долго и нудно проверять, не упадёт ли чего.
|
||
|
||
Кроме того, иногда проверить просто невозможно, например `JSON.parse(str)` не позволяет "проверить" формат строки перед разбором. В этом случае блок `try..catch` необходим.
|
||
|
||
Полный вид конструкции:
|
||
|
||
```js
|
||
*!*try*/!* {
|
||
.. пробуем выполнить код ..
|
||
} *!*catch*/!*(e) {
|
||
.. перехватываем исключение ..
|
||
} *!*finally*/!* {
|
||
.. выполняем всегда ..
|
||
}
|
||
```
|
||
|
||
Возможны также варианты `try..catch` или `try..finally`.</li>
|
||
<li>Оператор `throw err` генерирует свою ошибку, в качестве `err` рекомендуется использовать объекты, совместимые с встроенным типом [Error](http://javascript.ru/Error), содержащие свойства `message` и `name`.</li>
|
||
</ul>
|
||
|
||
Кроме того, мы рассмотрели некоторые важные приёмы:
|
||
|
||
<ul>
|
||
<li>Проброс исключения -- `catch(err)` должен обрабатывать только те ошибки, которые мы рассчитываем в нём увидеть, остальные -- пробрасывать дальше через `throw err`.
|
||
|
||
Определить, нужная ли это ошибка, можно, например, по свойству `name`.</li>
|
||
<li>Оборачивание исключений -- функция, в процессе работы которой возможны различные виды ошибок, может "обернуть их" в одну общую ошибку, специфичную для её задачи, и уже её пробросить дальше. Чтобы, при необходимости, можно было подробно определить, что произошло, исходную ошибку обычно присваивают в свойство этой, общей. Обычно это нужно для логирования.</li>
|
||
<li>В `window.onerror` можно присвоить функцию, которая выполнится при любой "выпавшей" из скрипта ошибке. Как правило, это используют в информационных целях, например отправляют информацию об ошибке на специальный сервис.</li>
|
||
</ul>
|
||
|
||
|