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

28 KiB
Raw Blame History

Перехват ошибок, "try..catch"

Как бы мы хорошо ни программировали, в коде бывают ошибки. Или, как их иначе называют, "исключительные ситуации" (исключения).

Обычно скрипт при ошибке, как говорят, "падает", с выводом ошибки в консоль.

Но бывают случаи, когда нам хотелось бы как-то контролировать ситуацию, чтобы скрипт не просто "упал", а сделал что-то разумное.

Для этого в JavaScript есть замечательная конструкция try..catch.

[cut]

Конструкция try..catch

Конструкция try..catch состоит из двух основных блоков: try, и затем catch:

try {

  // код ...

} catch (err) {

  // обработка ошибки

}

Работает она так:

  1. Выполняется код внутри блока `try`.
  2. Если в нём ошибок нет, то блок `catch(err)` игнорируется, то есть выполнение доходит до конца `try` и потом прыгает через `catch`.
  3. Если в нём возникнет ошибка, то выполнение `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' );
}

У него два варианта работы:

  1. Если вы ответите на вопрос "Сгенерировать ошибку?" утвердительно, то `try -> catch -> finally`.
  2. Если ответите отрицательно, то `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` можно присвоить функцию, которая выполнится при любой "выпавшей" из скрипта ошибке. Как правило, это используют в информационных целях, например отправляют информацию об ошибке на специальный сервис.