en.javascript.info/1-js/7-js-misc/5-exception/article.md
2015-04-23 12:31:37 +03:00

590 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Перехват ошибок, "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>