Исключения — мощное встроенное средство по обработке ошибок и управлению потоком выполнения.
Перехват ошибок в коде
Ошибки в коде бывают двух типов.
- Синтаксические — когда нарушена структура кода, например:
for (a = 5) { // неправильный аргумент for ]; // неправильная закрывающая скобкаТакой код браузер не может разобрать и выполнить.Их также называют «ошибки времени компиляции», поскольку они происходят на этапе разбора кода.
- Семантические, также называемые «ошибки времени выполнения» — возникают, когда браузер смог прочитать скрипт и уже выполняет код, но вдруг натыкается на проблему.
Например, «не определена переменная»:
alert(nonexistant); // переменная не определена.
Стандартный метод контроля ошибок — предварительная проверка. Мы можем посмотреть, существует ли переменная перед обращением к ней.
Но предварительная проверка возможна не всегда.
Например, если в документе есть IFRAME, то к его данным можно обратиться, но только если в нём открыт документ с того же домена. Иначе будет ошибка.
Эта мера направлена на защиту посетителей, чтобы страница со «злого сайта» не могла открыть в себе IFRAME, скажем, с почтой и, после того, как посетитель войдёт, прочитать из него все письма.
Представим, что у нас есть IFRAME и мы предполагаем, что можем в него залезть, но не уверены. Ограничение безопасности браузера сработает даже в том случае, если мы попробуем прочитать его URL — будет ошибка.
Пример (не выведет location из-за ошибки):
<iframe src="http://example.com" style="height:60px"></iframe>
<script>
onload = function() { // запустится при полной загрузке
*!*
alert(frames[0].location); // будет ошибка
*/!*
}
</script>
Вполне реальна ситуация, когда есть IFRAME, и мы хотим получить информацию из него, но, возможно, посетитель перешел в нём на другой домен. Тогда при попытке получить данные возникнет ошибка и скрипт «рухнет».
Как сделать, чтобы скрипт смог получить информацию об ошибке и продолжить работу?
…Как раз здесь нам поможет try..catch!
Конструкция try..catch
Конструкция try..catch состоит из двух основных блоков: try, и затем catch. Например:
try {
alert(frames[0].location);
...
alert('Выполнено без ошибок!');
} catch(e) {
alert('Ошибка!');
}
Работает это так:
- Выполняется код внутри блока
try. - Если в нём возникнет ошибка, то выполнение
tryпрерывается, и управление прыгает в начало блокаcatch.Например:
try { alert('Начало блока try'); // *!*(1) <--*/!* *!* lalala; // ошибка, переменная не определена! */!* alert('Конец блока try'); } catch(e) { alert('Управление получил блок catch!'); // *!*(2) <--*/!* }Будут два
alert'а:(1)и(2). - Если ошибки нет — блок
catchигнорируется.Например:
try { alert('Начало блока try'); // *!*(1) <--*/!* // .. код без ошибок alert('Конец блока try'); // *!*(2) <--*/!* } catch(e) { alert('Блок catch не получит управление'); }Будут два
alert'а:(1)и(2). Блокcatchне будет использован.
Логика работы try..catch позволяет отловить любые семантические ошибки.
В том числе, обработать ошибку при чтении из IFRAME'а:
<iframe src="http://example.com" style="height:60px"></iframe>
<script>
onload = function() {
*!*
try {
alert(frames[0].location);
} catch(exception) {
alert('Ой, а ифрейм-то на другом домене...');
}
*/!*
}
</script>
Объект ошибки
У блока catch есть аргумент, в примере выше он обозначен exception. Это — объект ошибки или, как ещё говорят, объект исключения (exception object).
Он содержит информацию о том, что произошло и может быть разным, в зависимости от ошибки.
Как правило, это объект встроенного типа Error и производных от него.
Есть несколько свойств, которые присутствуют в объекте ошибки во всех браузерах:
name- Тип ошибки. Например, при обращении к несуществующей переменной равен
"ReferenceError". message- Текстовое сообщение о деталях ошибки.
В зависимости от браузера, у него могут быть и дополнительные свойства, см. например Error в MDN и Error в MSDN.
В примере ниже идёт попытка вызова числовой переменной как функции. Это ошибка типа name = "TypeError", с сообщением message, которое зависит от браузера:
try {
var a = 5;
var res = a(1); // ошибка!
} catch(e) {
alert("name:" + e.name + "\nmessage:" + e.message);
}
Секция finally
Конструкция try..catch может содержать ещё один блок: finally. Выглядит этот расширенный синтаксис так:
*!*try*/!* {
.. пробуем выполнить код ..
} *!*catch*/!*(e) {
.. перехватываем исключение ..
} *!*finally*/!* {
.. выполняем всегда ..
}
Секция finally не обязательна, но если она есть, то она выполняется всегда:
- после блока
try, если ошибок не было, - после
catch, если они были.
Её используют, чтобы завершить начатые операции и очистить ресурсы, которые должны быть очищены в любом случае — как при ошибке, так и при нормальном потоке выполнения.
var tmpObject = ... // создать что-то, что нужно будет очистить
try {
.. поработать с tmpObject ..
} catch(e) {
.. обработать ошибку ..
} finally {
.. // очистить tmpObject
}
finally и return
Секция finally срабатывает при любом выходе из try..catch, в том числе и return.
В примере ниже, из try происходит return, но finally получает управление до того, как контроль возвращается во внешний код.
function func() {
try {
// сразу вернуть значение
return 1;
} catch(e) {
alert('Сюда управление не попадёт, ошибок нет');
} finally {
*!*
alert('Вызов завершён');
*/!*
}
}
alert( func() );
Это гарантирует освобождение ресурсов в любом случае.
Возможна также форма try..finally, без catch:
// редко используется, но тоже можно
try {
..
} finally {
..
}
Генерация своих ошибок
Перед тем, как перейти к генерации ошибок, начнём с небольшого примера, который позволит понять, зачем это нужно.
Пример: проверка значений
Ошибки могут быть частью нормального потока выполнения скрипта.
Например, они могут возникать в процессе проверки значений формы. Функция получает то, что ввёл посетитель и, в простейшем случае, возвращает true/false:
function checkValidAge(age) {
if (age >= 18) {
return true; // OK
} else {
return false; // Ошибка
}
}
..Но в случае обработки данных из формы простого true/false может быть недостаточно! Если проверка достаточно сложная, то хорошо бы вернуть ошибку, с текстовым описанием, что именно не так.
Решение без исключений (плохое!) выглядит так:
// функция возвращает:
// false, если всё хорошо
// или строку с ошибкой
function checkValidAge(age) {
if (age >= 18) {
return false;
} else {
return "Вы слишком молоды. Приходите через " + (18-age) + " лет";
}
}
..То есть, возврат false используется в смысле «всё в порядке», а строка означает ошибку. Это позволяет использовать функцию так:
var error = checkValidAge(age);
if (error) {
// показать ошибку
}
Почему это решение плохое?
В данном случае оно работает, и даже может показаться симпатичным, но что, если строка является нормальным результатом функции? Как тогда обозначить ошибку? Вернуть специальный объект?..
Зачем придумывать? Ответ уже есть — и это исключения! Генерация своей ошибки предоставляет альтернативный подход к проблеме, который мы сейчас рассмотрим.
Генерация ошибки: throw
Синтаксис: throw <объект ошибки>.
Инициирует ошибку, которую можно поймать в catch. Объектом ошибки может быть что угодно, например число:
try {
throw 123;
} catch(e) {
alert(e); // 123
}
Обычно, всё же, используют более информативные объекты, как минимум с теми же свойствами name, message, что и у встроенного Error.
Перепишем функции, которые осуществляют проверки, с использованием throw:
function BadValueError(name, message) {
this.name = "BadValue";
this.message = message;
}
function checkValidAge(age) {
if (age < 18) {
throw new BadValueError("Возраст не подходит");
}
}
function checkRequired(value) {
if (value == '') {
throw new BadValueError("Отсутствует значение");
}
}
Код для проверки:
var value = ageInput.value;
try {
checkRequired(value);
checkInteger(value);
checkValidAge(value);
/* ввод успешен */
} catch(e) {
/* обработать ошибку */
}
Гораздо короче, не правда ли? И при этом надёжнее, т.к. try..catch поймают любую ошибку, даже если посетитель ввёл что-то, от чего в функции возникла не предусмотренная программная ошибка.
Вложенные вызовы и try..catch
Брошенное исключение выпадает из всех циклов, функций и т.п.
Это — особое, уникальное свойство исключений.
Оно означает, что в случае вложенных вызовов — не важно, где было исключение, оно будет поймано внешней функцией.
Например:
function checkAll(value) {
checkRequired(value);
checkInteger(value);
checkValidAge(value);
}
try {
checkAll(value);
alert('Да, вы нам подходите!');
} catch(e) {
*!*
if (e.name == "BadValue") {
// ок, я знаю что делать с этой ошибкой
alert('Ошибка '+e.message);
} else {
// ошибка неизвестна, пробросить ее дальше
throw e;
*/!*
}
}
В примере выше дополнительно использована техника «проверки и проброса» исключения. Её смысл в том, что исключения могут быть самыми разными. Возможно, это ошибка проверки, а может быть — нет прав на действие, а может быть — доступ к необъявленной переменной… Возможно что угодно.
Функция checkAll умеет обрабатывать только ошибку проверки BadValue, что и делает. В том случае, если исключение какое-то другое, то она «пробрасывает» (throw e) его дальше, так что оно либо попадёт во внешнюю функцию, которая знает, что с ним делать, либо выпадает из скрипта как ошибка — и тогда разработчик увидит его в консоли.
Синтаксис try..finally (без catch) применяется тогда, когда мы хотим произвести очистку в finally, но совсем не умеем обрабатывать ошибки.
Итого
Исключения в JavaScript нужны редко. Но там, где они нужны — они нужны.
- Основная область применения — попытка сделать что-то, что может вернуть ошибку, но проверить заранее нельзя. Например, получить информацию из
IFRAME'а. - Другая область применения — завернуть блок кода, в котором может быть ошибка, в конструкцию
try..catch, и вместо многочисленных проверок обработать ошибку один раз, внизу.Мы видели это в примере с
checkRequired/checkInteger/checkAge.
Ошибка, генерируемая throw, «выпадает» из функции, и передаёт управление на ближайший catch:
function a() { b(); }
function b() { c(); }
function c() { throw new Error("Ошибка!"); }
*!*
try {
a(); // a() -> вызывает b() -> вызывает c() -> Ошибка!
} catch(e) {
alert(e); // <-- ошибка выпадет сюда
}
*/!*
Это делает try..catch + throw мощным средством контроля выполнения.
При использовании своих ошибок рекомендуется генерировать объекты встроенного типа Error или, когда мы познакомимся с наследованием в JavaScript — наследующие от него.
Ваши объекты также могут содержать дополнительную информацию об обстоятельствах ошибки, полезную для её обработки:
var err = new Error("Ошибка");
*!*
err.extra = new Date();
*/!*
alert(err.message + " в " + err.extra); // "Ошибка в (дата)"
Напишите интерфейс, который принимает математическое выражение (prompt) в и возвращает его результат.
Интерфейс может допускать использование любых функций.
При ошибке нужно выводить сообщение и просить переввести выражение. Ошибкой считается некорректное выражение, такое как 2+ и выражение, возвращающее NaN, например 0/0.
Демо в новом окне: tutorial/intro/eval-calc-try.html.
Вычислить любое выражение нам поможет eval:
alert( eval("2+2") ); // 4
Считываем выражение в цикле while(true). Если при вычислении возникает ошибка — ловим её в try..catch.
Ошибкой считается, в том числе, получение NaN из eval, хотя при этом исключение не возникает. Можно бросить своё исключение в этом случае.
while(true) {
expr = prompt("Введите выражение?", '2-');
try {
res = eval(expr); // при ошибке будет catch
if (isNaN(res)) { // наша ошибка
throw new Error("Результат неопределён");
}
break; // все ок, выход из цикла
} catch(e) {
alert("Ошибка: "+e.message+", повторите ввод");
}
}
alert(res);
Полное решение: tutorial/intro/eval-calc-try.html.
Комментарии
- Приветствуются комментарии, содержащие дополнения и вопросы по статье, и ответы на них.
- Если ваш комментарий касается задачи -- откройте её в отдельном окне и напишите там.
- Комментарии без смысла, с рекламой или не о статье вообще - удаляются.