Проброс ошибок js

JavaScript поддерживает компактный набор инструкций, особенно управляющих инструкций, которые вы можете использовать, чтобы реализовать интерактивность в вашем приложении. В данной главе даётся обзор этих инструкций.

Порядок выполнения и обработка ошибок

  • « Предыдущая статья
  • Следующая статья »

JavaScript поддерживает компактный набор инструкций, особенно управляющих инструкций, которые вы можете использовать, чтобы реализовать интерактивность в вашем приложении. В данной главе даётся обзор этих инструкций.

Более подробная информация об инструкциях, рассмотренных в данной главе, содержится в справочнике по JavaScript. Точка с запятой (;) используется для разделения инструкций в коде.

Любое выражение (expression) в JavaScript является также инструкцией (statement). Чтобы получить более подробную информацию о выражениях, прочитайте Выражения и операторы.

Инструкция block

Инструкция block является фундаментальной и используется для группировки других инструкций. Блок ограничивается фигурными скобками:

{ statement_1; statement_2; ... statement_n; }

Блок обычно используется с управляющими инструкциями (например, if, for, while).

В вышеприведённом примере { x++; } является блоком.

Обратите внимание: в JavaScript отсутствует область видимости блока до ECMAScript2015. Переменные, объявленные внутри блока, имеют область видимости функции (или скрипта), в которой находится данный блок, вследствие чего они сохранят свои значения при выходе за пределы блока. Другими словами, блок не создаёт новую область видимости. «Автономные» (standalone) блоки в JavaScript могут продуцировать полностью отличающийся результат, от результата в языках C или Java. Например:

var x = 1;
{
  var x = 2;
}
console.log(x); // выведет 2

В вышеприведённом примере инструкция var x внутри блока находится в той же области видимости, что и инструкция var x перед блоком. В C или Java эквивалентный код выведет значение 1.

Начиная с ECMAScript 6, оператор let позволяет объявить переменную в области видимости блока. Чтобы получить более подробную информацию, прочитайте let.

Условные инструкции

Условная инструкция — это набор команд, которые выполняются, если указанное условие является истинным. JavaScript поддерживает две условные инструкции: if...else и switch.

Инструкция if…else

Используйте оператор if для выполнения инструкции, если логическое условия истинно. Используйте опциональный else, для выполнения инструкции, если условие ложно. Оператор if выглядит так:

if (condition) {
    statement_1;
} else {
    statement_2;
}

Здесь condition может быть любым выражением, вычисляемым как истинное (true) или ложное (false). Чтобы получить более подробную информацию о значениях true и false, прочитайте Boolean. Если условие оценивается как true, то выполняется statement_1, в противном случае — statement_2. Блоки statement_1 и statement_2 могут быть любыми блоками, включая также вложенные инструкции if.

Также вы можете объединить несколько инструкций, пользуясь else if для получения последовательности проверок условий:

if (condition_1) { statement_1;} else if (condition_2) { statement_2;} else if (condition_n) { statement_n; } else { statement_last;}

В случае нескольких условий только первое логическое условие, которое вычислится истинным (true), будет выполнено. Используйте блок ({ ... }) для группировки нескольких инструкций. Применение блоков является хорошей практикой, особенно когда используются вложенные инструкции if:

if (condition) {
  statement_1_runs_if_condition_is_true;
  statement_2_runs_if_condition_is_true;
} else {
  statement_3_runs_if_condition_is_false;
  statement_4_runs_if_condition_is_false;
}

Нежелательно использовать простые присваивания в условном выражении, т.к. присваивание может быть спутано с равенством при быстром просмотре кода. Например, не используйте следующий код:

Если вам нужно использовать присваивание в условном выражении, то распространённой практикой является заключение операции присваивания в дополнительные скобки. Например:

if ( (x = y) ) { /* ... */ }

Ложные значения

Следующие значения являются ложными:

  • false
  • undefined
  • null
  • 0
  • NaN
  • пустая строка ( "" )

Все остальные значения, включая все объекты, будут восприняты как истина при передаче в условное выражение.

Не путайте примитивные логические значения true и false со значениями true и false объекта Boolean. Например:

var b = new Boolean(false);
if (b) // это условие true
if (b == true) // это условие false

В следующем примере функция checkData возвращает true, если число символов в объекте Text равно трём; в противном случае функция отображает окно alert и возвращает false.

function checkData() {
  if (document.form1.threeChar.value.length == 3) {
    return true;
  } else {
    alert("Enter exactly three characters. " +
    document.form1.threeChar.value + " is not valid.");
    return false;
  }
}

Инструкция switch

Инструкция switch позволяет сравнить значение выражения с различными вариантами и при совпадении выполнить соответствующий код. Инструкция имеет следующий вид:

switch (expression) {
   case label_1:
      statements_1
      [break;]
   case label_2:
      statements_2
      [break;]
   ...
   default:
      statements_default
      [break;]
}

Сначала производится поиск ветви case с меткой label, совпадающей со значением выражения expression. Если совпадение найдено, то соответствующий данной ветви код выполняется до оператора break, который прекращает выполнение switch и передаёт управление дальше. В противном случае управление передаётся необязательной ветви default и выполняется соответствующий ей код. Если ветвь default не найдена, то программа продолжит выполняться со строчки, следующей за инструкцией switch. По соглашению ветвь default является последней ветвью, но следовать этому соглашению необязательно.

Если оператор break отсутствует, то после выполнения кода, который соответствует выбранной ветви, начнётся выполнение кода, который следует за ней.

В следующем примере если fruittype имеет значение "Bananas", то будет выведено сообщение "Bananas are $0.48 a pound." и оператор break прекратит выполнение switch. Если бы оператор break отсутствовал, то был бы также выполнен код, соответствующий ветви "Cherries", т.е. выведено сообщение "Cherries are $3.00 a pound.".

switch (fruittype) {
  case "Oranges":
    console.log("Oranges are $0.59 a pound.");
    break;
  case "Apples":
    console.log("Apples are $0.32 a pound.");
    break;
  case "Bananas":
    console.log("Bananas are $0.48 a pound.");
    break;
  case "Cherries":
    console.log("Cherries are $3.00 a pound.");
    break;
  case "Mangoes":
    console.log("Mangoes are $0.56 a pound.");
    break;
  case "Papayas":
    console.log("Mangoes and papayas are $2.79 a pound.");
    break;
  default:
   console.log("Sorry, we are out of " + fruittype + ".");
}
console.log("Is there anything else you'd like?");

Инструкции обработки исключений

Инструкция throw используется, чтобы выбросить исключение, а инструкция try...catch, чтобы его обработать.

Типы исключений

Практически любой объект может быть выброшен как исключение. Тем не менее, не все выброшенные объекты создаются равными. Обычно числа или строки выбрасываются как исключения, но часто более эффективным является использование одного из типов исключений, специально созданных для этой цели:

  • Исключения ECMAScript
  • DOMException (en-US) и DOMError (en-US)

Инструкция throw

Используйте инструкцию throw, чтобы выбросить исключение. При выбросе исключения нужно указать выражение, содержащее значение, которое будет выброшено:

throw expression;

Вы можете выбросить любое выражение, а не только выражения определённого типа. В следующем примере выбрасываются исключения различных типов:

throw "Error2";                                              // string
throw 42;                                                    // number
throw true;                                                  // boolean
throw { toString: function() { return "I'm an object!"; } }; // object

Примечание: Вы можете выбросить объект как исключение. Вы можете обращаться к свойствам данного объекта в блоке catch.

Примечание: В следующем примере объект UserException выбрасывается как исключение:

function UserException (message) {
  this.message = message;
  this.name = "UserException";
}

UserException.prototype.toString = function () {
  return this.name + ': "' + this.message + '"';
}

throw new UserException("Value too high");

Инструкция try…catch

Инструкция try...catch состоит из блока try, который содержит одну или несколько инструкций, и блок catch, которые содержит инструкции, определяющие порядок действий при выбросе исключения в блоке try. Иными словами, если в блоке try будет выброшено исключение, то управление будет передано в блок catch. Если в блоке try не возникнет исключений, то блок catch будет пропущен. Блок finally будет выполнен после окончания работы блоков try и catch, вне зависимости от того, было ли выброшено исключение.

В следующем примере вызывается функция getMonthName, которая возвращает название месяца по его номеру. Если месяца с указанным номером не существует, то функция выбросит исключение "InvalidMonthNo", которое будет перехвачено в блоке catch:

function getMonthName(mo) {
  mo = mo - 1; // Adjust month number for array index (1 = Jan, 12 = Dec)
  var months = ["Jan","Feb","Mar","Apr","May","Jun","Jul",
                "Aug","Sep","Oct","Nov","Dec"];
  if (months[mo]) {
    return months[mo];
  } else {
    throw "InvalidMonthNo"; //throw keyword is used here
  }
}

try { // statements to try
  monthName = getMonthName(myMonth); // function could throw exception
}
catch (e) {
  monthName = "unknown";
  logMyErrors(e); // pass exception object to error handler -> your own
}

Блок catch

Используйте блок catch, чтобы обработать исключения, сгенерированные в блоке try.

catch (catchID) { statements }

JavaScript создаёт идентификатор catchID, которому присваивается перехваченное исключение, при входе в блок catch; данный идентификатор доступен только в пределах блока catch и уничтожается при выходе из него.

В следующем примере выбрасывается исключение, которое перехватывается в блоке catch:

try {
  throw "myException"
} catch (e) {
  console.error(e);
}

Блок finally

Блок finally содержит код, который будет выполнен после окончания работы блоков try и catch, но до того, как будет выполнен код, который следует за инструкцией try...catch. Блок finally выполняется вне зависимости от того, было ли выброшено исключение. Блок finally выполняется даже в том случае, если исключение не перехватывается в блоке catch.

В следующем примере открывается файл, затем в блоке try происходит вызов функции writeMyFile, который может выбросить исключение. Если возникает исключение, то оно обрабатывается в блоке catch. В любом случае файл будет закрыт функцией closeMyFile, вызов которой находится в блоке finally.

openMyFile();
try {
  writeMyFile(theData);
} catch(e) {
  handleError(e);
} finally {
  closeMyFile();
}

Если блок finally возвращает значение, то данное значение становится возвращаемым значением всей связки try-catch-finally. Значения, возвращаемые блоками try и catch, будут проигнорированы.

function f() {
  try {
    console.log(0);
    throw "bogus";
  } catch(e) {
    console.log(1);
    return true;    // приостанавливается до завершения блока `finally`
    console.log(2); // не выполняется
  } finally {
    console.log(3);
    return false;   // заменяет предыдущий `return`
    console.log(4); // не выполняется
  }
  // `return false` выполняется сейчас
  console.log(5);  // не выполняется
}
f();               // отображает 0, 1, 3 и возвращает `false`

Замена возвращаемых значений блоком finally распространяется в том числе и на исключения, которые выбрасываются или перевыбрасываются в блоке catch:

function f() {
  try {
    throw "bogus";
  } catch(e) {
    console.log('caught inner "bogus"');
    throw e;      // приостанавливается до завершения блока `finally`
  } finally {
    return false; // заменяет предыдущий `throw`
  }
  // `return false` выполняется сейчас
}

try {
  f();
} catch(e) {
  // Не выполняется, т.к. `throw` в `catch `заменяется на `return` в `finally`
  console.log('caught outer "bogus"');
}

// В результате отображается сообщение caught inner "bogus"
// и возвращается значение `false`

Вложенные инструкции try...catch

Вы можете вкладывать инструкции try...catch друг в друга. Если внутренняя инструкция try...catch не имеет блока catch, то она должна иметь блок finally, кроме того исключение будет перехвачено во внешнем блоке catch. Для получения большей информации ознакомьтесь с вложенными try-блоками.

Использование объекта Error

В зависимости от типа ошибки вы можете использовать свойства name и message, чтобы получить более подробную информацию. Свойство name содержит название ошибки (например, DOMException или Error), свойство message — описание ошибки.

Если вы выбрасываете собственные исключения, то чтобы получить преимущество, которое предоставляют эти свойства (например, если ваш блок catch не делает различий между вашими исключениями и системными), используйте конструктор Error. Например:

function doSomethingErrorProne () {
  if ( ourCodeMakesAMistake() ) {
    throw ( new Error('The message') );
  } else {
    doSomethingToGetAJavascriptError();
  }
}

try {
  doSomethingErrorProne();
} catch (e) {
  console.log(e.name);    // 'Error'
  console.log(e.message); // 'The message' или JavaScript error message
}

Объект Promise

Начиная с ECMAScript2015, JavaScript поддерживает объект Promise, который используется для отложенных и асинхронных операций.

Объект Promise может находиться в следующих состояниях:

  • ожидание (pending): начальное состояние, не выполнено и не отклонено.
  • выполнено (fulfilled): операция завершена успешно.
  • отклонено (rejected): операция завершена с ошибкой.
  • заданный (settled): промис выполнен или отклонен, но не находится в состоянии ожидания.

Загрузка изображения при помощи XHR

Простой пример использования объектов Promise и XMLHttpRequest для загрузки изображения доступен в репозитории MDN promise-test на GitHub. Вы также можете посмотреть его в действии. Каждый шаг прокомментирован, что позволяет вам разобраться в архитектуре Promise и XHR. Здесь приводится версия без комментариев:

function imgLoad(url) {
  return new Promise(function(resolve, reject) {
    var request = new XMLHttpRequest();
    request.open('GET', url);
    request.responseType = 'blob';
    request.onload = function() {
      if (request.status === 200) {
        resolve(request.response);
      } else {
        reject(Error('Image didn't load successfully; error code:'
                     + request.statusText));
      }
    };
    request.onerror = function() {
      reject(Error('There was a network error.'));
    };
    request.send();
  });
}
  • « Предыдущая статья
  • Следующая статья »

Ошибки — это хорошо. Автор материала, перевод которого мы сегодня публикуем, говорит, что уверен в том, что эта идея известна всем. На первый взгляд ошибки кажутся чем-то страшным. Им могут сопутствовать какие-то потери. Ошибка, сделанная на публике, вредит авторитету того, кто её совершил. Но, совершая ошибки, мы на них учимся, а значит, попадая в следующий раз в ситуацию, в которой раньше вели себя неправильно, делаем всё как нужно.

Выше мы говорили об ошибках, которые люди совершают в обычной жизни. Ошибки в программировании — это нечто иное. Сообщения об ошибках помогают нам улучшать код, они позволяют сообщать пользователям наших проектов о том, что что-то пошло не так, и, возможно, рассказывают пользователям о том, как нужно вести себя для того, чтобы ошибок больше не возникало.

Этот материал, посвящённый обработке ошибок в JavaScript, разбит на три части. Сначала мы сделаем общий обзор системы обработки ошибок в JavaScript и поговорим об объектах ошибок. После этого мы поищем ответ на вопрос о том, что делать с ошибками, возникающими в серверном коде (в частности, при использовании связки Node.js + Express.js). Далее — обсудим обработку ошибок в React.js. Фреймворки, которые будут здесь рассматриваться, выбраны по причине их огромной популярности. Однако рассматриваемые здесь принципы работы с ошибками универсальны, поэтому вы, даже если не пользуетесь Express и React, без труда сможете применить то, что узнали, к тем инструментам, с которыми работаете.

Код демонстрационного проекта, используемого в данном материале, можно найти в этом репозитории.

1. Ошибки в JavaScript и универсальные способы работы с ними

Если в вашем коде что-то пошло не так, вы можете воспользоваться следующей конструкцией.

throw new Error('something went wrong')

В ходе выполнения этой команды будет создан экземпляр объекта Error и будет сгенерировано (или, как говорят, «выброшено») исключение с этим объектом. Инструкция throw может генерировать исключения, содержащие произвольные выражения. При этом выполнение скрипта остановится в том случае, если не были предприняты меры по обработке ошибки.

Начинающие JS-программисты обычно не используют инструкцию throw. Они, как правило, сталкиваются с исключениями, выдаваемыми либо средой выполнения языка, либо сторонними библиотеками. Когда это происходит — в консоль попадает нечто вроде ReferenceError: fs is not defined и выполнение программы останавливается.

▍Объект Error

У экземпляров объекта Error есть несколько свойств, которыми мы можем пользоваться. Первое интересующее нас свойство — message. Именно сюда попадает та строка, которую можно передать конструктору ошибки в качестве аргумента. Например, ниже показано создание экземпляра объекта Error и вывод в консоль переданной конструктором строки через обращение к его свойству message.

const myError = new Error('please improve your code')
console.log(myError.message) // please improve your code

Второе свойство объекта, очень важное, представляет собой трассировку стека ошибки. Это — свойство stack. Обратившись к нему можно просмотреть стек вызовов (историю ошибки), который показывает последовательность операций, приведшую к неправильной работе программы. В частности, это позволяет понять — в каком именно файле содержится сбойный код, и увидеть, какая последовательность вызовов функций привела к ошибке. Вот пример того, что можно увидеть, обратившись к свойству stack.

Error: please improve your code
 at Object.<anonymous> (/Users/gisderdube/Documents/_projects/hacking.nosync/error-handling/src/general.js:1:79)
 at Module._compile (internal/modules/cjs/loader.js:689:30)
 at Object.Module._extensions..js (internal/modules/cjs/loader.js:700:10)
 at Module.load (internal/modules/cjs/loader.js:599:32)
 at tryModuleLoad (internal/modules/cjs/loader.js:538:12)
 at Function.Module._load (internal/modules/cjs/loader.js:530:3)
 at Function.Module.runMain (internal/modules/cjs/loader.js:742:12)
 at startup (internal/bootstrap/node.js:266:19)
 at bootstrapNodeJSCore (internal/bootstrap/node.js:596:3)

Здесь, в верхней части, находится сообщение об ошибке, затем следует указание на тот участок кода, выполнение которого вызвало ошибку, потом описывается то место, откуда был вызван этот сбойный участок. Это продолжается до самого «дальнего» по отношению к ошибке фрагмента кода.

▍Генерирование и обработка ошибок

Создание экземпляра объекта Error, то есть, выполнение команды вида new Error(), ни к каким особым последствиям не приводит. Интересные вещи начинают происходить после применения оператора throw, который генерирует ошибку. Как уже было сказано, если такую ошибку не обработать, выполнение скрипта остановится. При этом нет никакой разницы — был ли оператор throw использован самим программистом, произошла ли ошибка в некоей библиотеке или в среде выполнения языка (в браузере или в Node.js). Поговорим о различных сценариях обработки ошибок.

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

Блок try...catch представляет собой самый простой способ обработки ошибок, о котором часто забывают. В наши дни, правда, он используется гораздо интенсивнее чем раньше, благодаря тому, что его можно применять для обработки ошибок в конструкциях async/await.

Этот блок можно использовать для обработки любых ошибок, происходящих в синхронном коде. Рассмотрим пример.

const a = 5

try {
    console.log(b) // переменная b не объявлена - возникает ошибка
} catch (err) {
    console.error(err) // в консоль попадает сообщение об ошибке и стек ошибки
}

console.log(a) // выполнение скрипта не останавливается, данная команда выполняется

Если бы в этом примере мы не заключили бы сбойную команду console.log(b) в блок try...catch, то выполнение скрипта было бы остановлено.

▍Блок finally

Иногда случается так, что некий код нужно выполнить независимо от того, произошла ошибка или нет. Для этого можно, в конструкции try...catch, использовать третий, необязательный, блок — finally. Часто его использование эквивалентно некоему коду, который идёт сразу после try...catch, но в некоторых ситуациях он может пригодиться. Вот пример его использования.

const a = 5

try {
    console.log(b) // переменная b не объявлена - возникает ошибка
} catch (err) {
    console.error(err) // в консоль попадает сообщение об ошибке и стек ошибки
} finally {
    console.log(a) // этот код будет выполнен в любом случае
}

▍Асинхронные механизмы — коллбэки

Программируя на JavaScript всегда стоит обращать внимание на участки кода, выполняющиеся асинхронно. Если у вас имеется асинхронная функция и в ней возникает ошибка, скрипт продолжит выполняться. Когда асинхронные механизмы в JS реализуются с использованием коллбэков (кстати, делать так не рекомендуется), соответствующий коллбэк (функция обратного вызова) обычно получает два параметра. Это нечто вроде параметра err, который может содержать ошибку, и result — с результатами выполнения асинхронной операции. Выглядит это примерно так:

myAsyncFunc(someInput, (err, result) => {
    if(err) return console.error(err) // порядок работы с объектом ошибки мы рассмотрим позже
    console.log(result)
})

Если в коллбэк попадает ошибка, она видна там в виде параметра err. В противном случае в этот параметр попадёт значение undefined или null. Если оказалось, что в err что-то есть, важно отреагировать на это, либо так как в нашем примере, воспользовавшись командой return, либо воспользовавшись конструкцией if...else и поместив в блок else команды для работы с результатом выполнения асинхронной операции. Речь идёт о том, чтобы, в том случае, если произошла ошибка, исключить возможность работы с результатом, параметром result, который в таком случае может иметь значение undefined. Работа с таким значением, если предполагается, например, что оно содержит объект, сама может вызвать ошибку. Скажем, это произойдёт при попытке использовать конструкцию result.data или подобную ей.

▍Асинхронные механизмы — промисы

Для выполнения асинхронных операций в JavaScript лучше использовать не коллбэки а промисы. Тут, в дополнение к улучшенной читабельности кода, имеются и более совершенные механизмы обработки ошибок. А именно, возиться с объектом ошибки, который может попасть в функцию обратного вызова, при использовании промисов не нужно. Здесь для этой цели предусмотрен специальный блок catch. Он перехватывает все ошибки, произошедшие в промисах, которые находятся до него, или все ошибки, которые произошли в коде после предыдущего блока catch. Обратите внимание на то, что если в промисе произошла ошибка, для обработки которой нет блока catch, это не остановит выполнение скрипта, но сообщение об ошибке будет не особенно удобочитаемым.

(node:7741) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: something went wrong
(node:7741) DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code. */

В результате можно порекомендовать всегда, при работе с промисами, использовать блок catch. Взглянем на пример.

Promise.resolve(1)
    .then(res => {
        console.log(res) // 1

        throw new Error('something went wrong')

        return Promise.resolve(2)
    })
    .then(res => {
        console.log(res) // этот блок выполнен не будет
    })
    .catch(err => {
        console.error(err) // о том, что делать с этой ошибкой, поговорим позже
        return Promise.resolve(3)
    })
    .then(res => {
        console.log(res) // 3
    })
    .catch(err => {
        // этот блок тут на тот случай, если в предыдущем блоке возникнет какая-нибудь ошибка
        console.error(err)
    })

▍Асинхронные механизмы и try…catch

После того, как в JavaScript появилась конструкция async/await, мы вернулись к классическому способу обработки ошибок — к try...catch...finally. Обрабатывать ошибки при таком подходе оказывается очень легко и удобно. Рассмотрим пример.

;(async function() {
    try {
        await someFuncThatThrowsAnError()
    } catch (err) {
        console.error(err) // об этом поговорим позже
    }

    console.log('Easy!') // будет выполнено
})()

При таком подходе ошибки в асинхронном коде обрабатываются так же, как в синхронном. В результате теперь, при необходимости, в одном блоке catch можно обрабатывать более широкий диапазон ошибок.

2. Генерирование и обработка ошибок в серверном коде

Теперь, когда у нас есть инструменты для работы с ошибками, посмотрим на то, что мы можем с ними делать в реальных ситуациях. Генерирование и правильная обработка ошибок — это важнейший аспект серверного программирования. Существуют разные подходы к работе с ошибками. Здесь будет продемонстрирован подход с использованием собственного конструктора для экземпляров объекта Error и кодов ошибок, которые удобно передавать во фронтенд или любым механизмам, использующим серверные API. Как структурирован бэкенд конкретного проекта — особого значения не имеет, так как при любом подходе можно использовать одни и те же идеи, касающиеся работы с ошибками.

В качестве серверного фреймворка, отвечающего за маршрутизацию, мы будем использовать Express.js. Подумаем о том, какая структура нам нужна для организации эффективной системы обработки ошибок. Итак, вот что нам нужно:

  1. Универсальная обработка ошибок — некий базовый механизм, подходящий для обработки любых ошибок, в ходе работы которого просто выдаётся сообщение наподобие Something went wrong, please try again or contact us, предлагающее пользователю попробовать выполнить операцию, давшую сбой, ещё раз или связаться с владельцем сервера. Эта система не отличается особой интеллектуальностью, но она, по крайней мере, способна сообщить пользователю о том, что что-то пошло не так. Подобное сообщение гораздо лучше, чем «бесконечная загрузка» или нечто подобное.
  2. Обработка конкретных ошибок — механизм, позволяющий сообщить пользователю подробные сведения о причинах неправильного поведения системы и дать ему конкретные советы по борьбе с неполадкой. Например, это может касаться отсутствия неких важных данных в запросе, который пользователь отправляет на сервер, или в том, что в базе данных уже существует некая запись, которую он пытается добавить ещё раз, и так далее.

▍Разработка собственного конструктора объектов ошибок

Здесь мы воспользуемся стандартным классом Error и расширим его. Пользоваться механизмами наследования в JavaScript — дело рискованное, но в данном случае эти механизмы оказываются весьма полезными. Зачем нам наследование? Дело в том, что нам, для того, чтобы код удобно было бы отлаживать, нужны сведения о трассировке стека ошибки. Расширяя стандартный класс Error, мы, без дополнительных усилий, получаем возможности по трассировке стека. Мы добавляем в наш собственный объект ошибки два свойства. Первое — это свойство code, доступ к которому можно будет получить с помощью конструкции вида err.code. Второе — свойство status. В него будет записываться код состояния HTTP, который планируется передавать клиентской части приложения.

Вот как выглядит класс CustomError, код которого оформлен в виде модуля.

class CustomError extends Error {
    constructor(code = 'GENERIC', status = 500, ...params) {
        super(...params)

        if (Error.captureStackTrace) {
            Error.captureStackTrace(this, CustomError)
        }

        this.code = code
        this.status = status
    }
}

module.exports = CustomError

▍Маршрутизация

Теперь, когда наш объект ошибки готов к использованию, нужно настроить структуру маршрутов. Как было сказано выше, нам требуется реализовать унифицированный подход к обработке ошибок, позволяющий одинаково обрабатывать ошибки для всех маршрутов. По умолчанию фреймворк Express.js не вполне поддерживает такую схему работы. Дело в том, что все его маршруты инкапсулированы.

Для того чтобы справиться с этой проблемой, мы можем реализовать собственный обработчик маршрутов и определять логику маршрутов в виде обычных функций. Благодаря такому подходу, если функция маршрута (или любая другая функция) выбрасывает ошибку, она попадёт в обработчик маршрутов, который затем может передать её клиентской части приложения. При возникновении ошибки на сервере мы планируем передавать её во фронтенд в следующем формате, полагая, что для этого будет применяться JSON-API:

{
    error: 'SOME_ERROR_CODE',
    description: 'Something bad happened. Please try again or contact support.'
}

Если на данном этапе происходящие кажется вам непонятным — не беспокойтесь — просто продолжайте читать, пробуйте работать с тем, о чём идёт речь, и постепенно вы во всём разберётесь. На самом деле, если говорить о компьютерном обучении, здесь применяется подход «сверху-вниз», когда сначала обсуждаются общие идеи, а потом осуществляется переход к частностям.

Вот как выглядит код обработчика маршрутов.

const express = require('express')
const router = express.Router()
const CustomError = require('../CustomError')

router.use(async (req, res) => {
    try {
        const route = require(`.${req.path}`)[req.method]

        try {
            const result = route(req) // Передаём запрос функции route
            res.send(result) // Передаём клиенту то, что получено от функции route
        } catch (err) {
            /*
            Сюда мы попадаем в том случае, если в функции route произойдёт ошибка
            */
            if (err instanceof CustomError) {
                /* 
                Если ошибка уже обработана - трансформируем её в 
                возвращаемый объект
                */

                return res.status(err.status).send({
                    error: err.code,
                    description: err.message,
                })
            } else {
                console.error(err) // Для отладочных целей

                // Общая ошибка - вернём универсальный объект ошибки
                return res.status(500).send({
                    error: 'GENERIC',
                    description: 'Something went wrong. Please try again or contact support.',
                })
            }
        }
    } catch (err) {
        /* 
         Сюда мы попадём, если запрос окажется неудачным, то есть,
         либо не будет найдено файла, соответствующего пути, переданному
         в запросе, либо не будет экспортированной функции с заданным
         методом запроса
        */
        res.status(404).send({
            error: 'NOT_FOUND',
            description: 'The resource you tried to access does not exist.',
        })
    }
})

module.exports = router

Полагаем, комментарии в коде достаточно хорошо его поясняют. Надеемся, читать их удобнее, чем объяснения подобного кода, данные после него.

Теперь взглянем на файл маршрутов.

const CustomError = require('../CustomError')

const GET = req => {
    // пример успешного выполнения запроса
    return { name: 'Rio de Janeiro' }
}

const POST = req => {
    // пример ошибки общего характера
    throw new Error('Some unexpected error, may also be thrown by a library or the runtime.')
}

const DELETE = req => {
    // пример ошибки, обрабатываемой особым образом
    throw new CustomError('CITY_NOT_FOUND', 404, 'The city you are trying to delete could not be found.')
}

const PATCH = req => {
    // пример перехвата ошибок и использования CustomError
    try {
        // тут случилось что-то нехорошее
        throw new Error('Some internal error')
    } catch (err) {
        console.error(err) // принимаем решение о том, что нам тут делать

        throw new CustomError(
            'CITY_NOT_EDITABLE',
            400,
            'The city you are trying to edit is not editable.'
        )
    }
}

module.exports = {
    GET,
    POST,
    DELETE,
    PATCH,
}

В этих примерах с самими запросами ничего не делается. Тут просто рассматриваются разные сценарии возникновения ошибок. Итак, например, запрос GET /city попадёт в функцию const GET = req =>..., запрос POST /city попадёт в функцию const POST = req =>... и так далее. Эта схема работает и при использовании параметров запросов. Например — для запроса вида GET /city?startsWith=R. В целом, здесь продемонстрировано, что при обработке ошибок, во фронтенд может попасть либо общая ошибка, содержащая лишь предложение попробовать снова или связаться с владельцем сервера, либо ошибка, сформированная с использованием конструктора CustomError, которая содержит подробные сведения о проблеме.
Данные общей ошибки придут в клиентскую часть приложения в таком виде:

{
    error: 'GENERIC',
    description: 'Something went wrong. Please try again or contact support.'
}

Конструктор CustomError используется так:

throw new CustomError('MY_CODE', 400, 'Error description')

Это даёт следующий JSON-код, передаваемый во фронтенд:

{
    error: 'MY_CODE',
    description: 'Error description'
}

Теперь, когда мы основательно потрудились над серверной частью приложения, в клиентскую часть больше не попадают бесполезные логи ошибок. Вместо этого клиент получает полезные сведения о том, что пошло не так.

Не забудьте о том, что здесь лежит репозиторий с рассматриваемым здесь кодом. Можете его загрузить, поэкспериментировать с ним, и, если надо, адаптировать под нужды вашего проекта.

3. Работа с ошибками на клиенте

Теперь пришла пора описать третью часть нашей системы обработки ошибок, касающуюся фронтенда. Тут нужно будет, во-первых, обрабатывать ошибки, возникающие в клиентской части приложения, а во-вторых, понадобится оповещать пользователя об ошибках, возникающих на сервере. Разберёмся сначала с показом сведений о серверных ошибках. Как уже было сказано, в этом примере будет использована библиотека React.

▍Сохранение сведений об ошибках в состоянии приложения

Как и любые другие данные, ошибки и сообщения об ошибках могут меняться, поэтому их имеет смысл помещать в состояние компонентов. При монтировании компонента данные об ошибке сбрасываются, поэтому, когда пользователь впервые видит страницу, там сообщений об ошибках не будет.

Следующее, с чем надо разобраться, заключается в том, что ошибки одного типа нужно показывать в одном стиле. По аналогии с сервером, здесь можно выделить 3 типа ошибок.

  1. Глобальные ошибки — в эту категорию попадают сообщения об ошибках общего характера, приходящие с сервера, или ошибки, которые, например, возникают в том случае, если пользователь не вошёл в систему и в других подобных ситуациях.
  2. Специфические ошибки, выдаваемые серверной частью приложения — сюда относятся ошибки, сведения о которых приходят с сервера. Например, подобная ошибка возникает, если пользователь попытался войти в систему и отправил на сервер имя и пароль, а сервер сообщил ему о том, что пароль неправильный. Подобные вещи в клиентской части приложения не проверяются, поэтому сообщения о таких ошибках должны приходить с сервера.
  3. Специфические ошибки, выдаваемые клиентской частью приложения. Пример такой ошибки — сообщение о некорректном адресе электронной почты, введённом в соответствующее поле.

Ошибки второго и третьего типов очень похожи, работать с ними можно, используя хранилище состояния компонентов одного уровня. Их главное различие заключается в том, что они исходят из разных источников. Ниже, анализируя код, мы посмотрим на работу с ними.

Здесь будет использоваться встроенная в React система управления состоянием приложения, но, при необходимости, вы можете воспользоваться и специализированными решениями для управления состоянием — такими, как MobX или Redux.

▍Глобальные ошибки

Обычно сообщения о таких ошибках сохраняются в компоненте наиболее высокого уровня, имеющем состояние. Они выводятся в статическом элементе пользовательского интерфейса. Это может быть красное поле в верхней части экрана, модальное окно или что угодно другое. Реализация зависит от конкретного проекта. Вот как выглядит сообщение о такой ошибке.

Сообщение о глобальной ошибке

Теперь взглянем на код, который хранится в файле Application.js.

import React, { Component } from 'react'

import GlobalError from './GlobalError'

class Application extends Component {
    constructor(props) {
        super(props)

        this.state = {
            error: '',
        }

        this._resetError = this._resetError.bind(this)
        this._setError = this._setError.bind(this)
    }

    render() {
        return (
            <div className="container">
                <GlobalError error={this.state.error} resetError={this._resetError} />
                <h1>Handling Errors</h1>
            </div>
        )
    }

    _resetError() {
        this.setState({ error: '' })
    }

    _setError(newError) {
        this.setState({ error: newError })
    }
}

export default Application

Как видно, в состоянии, в Application.js, имеется место для хранения данных ошибки. Кроме того, тут предусмотрены методы для сброса этих данных и для их изменения.

Ошибка и метод для сброса ошибки передаётся компоненту GlobalError, который отвечает за вывод сообщения об ошибке на экран и за сброс ошибки после нажатия на значок x в поле, где выводится сообщение. Вот код компонента GlobalError (файл GlobalError.js).

import React, { Component } from 'react'

class GlobalError extends Component {
    render() {
        if (!this.props.error) return null

        return (
            <div
                style={{
                    position: 'fixed',
                    top: 0,
                    left: '50%',
                    transform: 'translateX(-50%)',
                    padding: 10,
                    backgroundColor: '#ffcccc',
                    boxShadow: '0 3px 25px -10px rgba(0,0,0,0.5)',
                    display: 'flex',
                    alignItems: 'center',
                }}
            >
                {this.props.error}
                 
                <i
                    className="material-icons"
                    style={{ cursor: 'pointer' }}
                    onClick={this.props.resetError}
                >
                    close
                </font></i>
            </div>
        )
    }
}

export default GlobalError

Обратите внимание на строку if (!this.props.error) return null. Она указывает на то, что при отсутствии ошибки компонент ничего не выводит. Это предотвращает постоянный показ красного прямоугольника на странице. Конечно, вы, при желании, можете поменять внешний вид и поведение этого компонента. Например, вместо того, чтобы сбрасывать ошибку по нажатию на x, можно задать тайм-аут в пару секунд, по истечении которого состояние ошибки сбрасывается автоматически.

Теперь, когда всё готово для работы с глобальными ошибками, для задания глобальной ошибки достаточно воспользоваться _setError из Application.js. Например, это можно сделать в том случае, если сервер, после обращения к нему, вернул сообщение об общей ошибке (error: 'GENERIC'). Рассмотрим пример (файл GenericErrorReq.js).

import React, { Component } from 'react'
import axios from 'axios'

class GenericErrorReq extends Component {
    constructor(props) {
        super(props)

        this._callBackend = this._callBackend.bind(this)
    }

    render() {
        return (
            <div>
                <button onClick={this._callBackend}>Click me to call the backend</button>
            </div>
        )
    }

    _callBackend() {
        axios
            .post('/api/city')
            .then(result => {
                // сделать что-нибудь с результатом в том случае, если запрос оказался успешным
            })
            .catch(err => {
                if (err.response.data.error === 'GENERIC') {
                    this.props.setError(err.response.data.description)
                }
            })
    }
}

export default GenericErrorReq

На самом деле, на этом наш разговор об обработке ошибок можно было бы и закончить. Даже если в проекте нужно оповещать пользователя о специфических ошибках, никто не мешает просто поменять глобальное состояние, хранящее ошибку и вывести соответствующее сообщение поверх страницы. Однако тут мы не остановимся и поговорим о специфических ошибках. Во-первых, это руководство по обработке ошибок иначе было бы неполным, а во-вторых, с точки зрения UX-специалистов, неправильно будет показывать сообщения обо всех ошибках так, будто все они — глобальные.

▍Обработка специфических ошибок, возникающих при выполнении запросов

Вот пример специфического сообщения об ошибке, выводимого в том случае, если пользователь пытается удалить из базы данных город, которого там нет.

Сообщение о специфической ошибке

Тут используется тот же принцип, который мы применяли при работе с глобальными ошибками. Только сведения о таких ошибках хранятся в локальном состоянии соответствующих компонентов. Работа с ними очень похожа на работу с глобальными ошибками. Вот код файла SpecificErrorReq.js.

import React, { Component } from 'react'
import axios from 'axios'

import InlineError from './InlineError'

class SpecificErrorRequest extends Component {
    constructor(props) {
        super(props)

        this.state = {
            error: '',
        }

        this._callBackend = this._callBackend.bind(this)
    }

    render() {
        return (
            <div>
                <button onClick={this._callBackend}>Delete your city</button>
                <InlineError error={this.state.error} />
            </div>
        )
    }

    _callBackend() {
        this.setState({
            error: '',
        })

        axios
            .delete('/api/city')
            .then(result => {
                // сделать что-нибудь с результатом в том случае, если запрос оказался успешным
            })
            .catch(err => {
                if (err.response.data.error === 'GENERIC') {
                    this.props.setError(err.response.data.description)
                } else {
                    this.setState({
                        error: err.response.data.description,
                    })
                }
            })
    }
}

export default SpecificErrorRequest

Тут стоит отметить, что для сброса специфических ошибок недостаточно, например, просто нажать на некую кнопку x. То, что пользователь прочёл сообщение об ошибке и закрыл его, не помогает такую ошибку исправить. Исправить её можно, правильно сформировав запрос к серверу, например — введя в ситуации, показанной на предыдущем рисунке, имя города, который есть в базе. В результате очищать сообщение об ошибке имеет смысл, например, после выполнения нового запроса. Сбросить ошибку можно и в том случае, если пользователь внёс изменения в то, что будет использоваться при формировании нового запроса, то есть — при изменении содержимого поля ввода.

▍Ошибки, возникающие в клиентской части приложения

Как уже было сказано, для хранения данных о таких ошибках можно использовать состояние тех же компонентов, которое используется для хранения данных по специфическим ошибкам, поступающим с сервера. Предположим, мы позволяем пользователю отправить на сервер запрос на удаление города из базы только в том случае, если в соответствующем поле ввода есть какой-то текст. Отсутствие или наличие текста в поле можно проверить средствами клиентской части приложения.

В поле ничего нет, мы сообщаем об этом пользователю

Вот код файла SpecificErrorFrontend.js, реализующий вышеописанный функционал.

import React, { Component } from 'react'
import axios from 'axios'

import InlineError from './InlineError'

class SpecificErrorRequest extends Component {
    constructor(props) {
        super(props)

        this.state = {
            error: '',
            city: '',
        }

        this._callBackend = this._callBackend.bind(this)
        this._changeCity = this._changeCity.bind(this)
    }

    render() {
        return (
            <div>
                <input
                    type="text"
                    value={this.state.city}
                    style={{ marginRight: 15 }}
                    onChange={this._changeCity}
                />
                <button onClick={this._callBackend}>Delete your city</button>
                <InlineError error={this.state.error} />
            </div>
        )
    }

    _changeCity(e) {
        this.setState({
            error: '',
            city: e.target.value,
        })
    }

    _validate() {
        if (!this.state.city.length) throw new Error('Please provide a city name.')
    }

    _callBackend() {
        this.setState({
            error: '',
        })

        try {
            this._validate()
        } catch (err) {
            return this.setState({ error: err.message })
        }

        axios
            .delete('/api/city')
            .then(result => {
                // сделать что-нибудь с результатом в том случае, если запрос оказался успешным
            })
            .catch(err => {
                if (err.response.data.error === 'GENERIC') {
                    this.props.setError(err.response.data.description)
                } else {
                    this.setState({
                        error: err.response.data.description,
                    })
                }
            })
    }
}

export default SpecificErrorRequest

▍Интернационализация сообщений об ошибках с использованием кодов ошибок

Возможно, сейчас вы задаётесь вопросом о том, зачем нам нужны коды ошибок (наподобие GENERIC), если мы показываем пользователю только сообщения об ошибках, полученных с сервера. Дело в том, что, по мере роста и развития приложения, оно, вполне возможно, выйдет на мировой рынок, а это означает, что настанет время, когда создателям приложения нужно будет задуматься о поддержке им нескольких языков. Коды ошибок позволяют отличать их друг от друга и выводить сообщения о них на языке пользователя сайта.

Итоги

Надеемся, теперь у вас сформировалось понимание того, как можно работать с ошибками в веб-приложениях. Нечто вроде console.error(err) следует использовать только в отладочных целях, в продакшн подобные вещи, забытые программистом, проникать не должны. Упрощает решение задачи логирования использование какой-нибудь подходящей библиотеки наподобие loglevel.

Уважаемые читатели! Как вы обрабатываете ошибки в своих проектах?

В предыдущих уроках мы с вами изучили два
места, в которых JavaScript выбрасывает исключение
в случае каких-то проблем.

В вашем проекте, однако, могут быть и другие
ситуации, которые для вас являются исключительными,
а для JavaScript — нет. В таком случае мы
можете создавать и выбрасывать свои, пользовательские
исключения.

Давайте разберем синтаксис, необходимый для
этого. Для начала исключение нужно создать
с помощью команды new Error, параметром
передав текст исключения:

new Error('текст исключения');

Затем это исключение нужно выбросить с помощью
команды throw:

throw new Error('текст исключения');

Выбрасывание исключение заставляет JavaScript
считать, что случилась исключительная ситуация.
Это значит, что такое исключение можно отловить
с помощью конструкции try-catch и
обработать нужным образом.

Давайте посмотрим на примере, как этим пользоваться.
Пусть у нас есть функция, которая делит одно
число на другое:

function div(a, b) {
return a / b;
}

Давайте будем считать, что деление на ноль
запрещено и при попытке сделать это должна
возникать исключительная ситуация. Для этого
будем проверять в функции, нет ли попытки
делить на 0. Если нет — будем делить,
а если нет — будем бросать исключение:

function div(a, b) {
if (b !== 0) {
return a / b;
} else {
throw new Error('ошибка деления на ноль');
}
}

Давайте для начала просто попробуем поделить
на 0, не перехватывая исключение:

alert( div(3, 0) );

В этом случае выполнение скрипта прервется
и в консоли появится ошибка с текстом 'ошибка
деления на ноль'
(проверьте). Давайте
теперь будем перехватывать нашу ошибку и
как-то ее обрабатывать:

try {
alert( div(3, 0) );
} catch (error) {
alert('вы пытаетесь делить на 0, что запрещено');
}

В JavaScript попытка извлечь корень из отрицательного
числа не приводит к выбрасыванию исключения:

let result = Math.sqrt(-1);
console.log(result); // выведет NaN

Напишите свою функцию, которая будет извлекать
корень из числа и при этом выбрасывать исключение,
если корень извлекается из отрицательного числа.

Типы исключений

Давайте выбросим свое исключение и посмотрим,
как будет вести себя объект с ошибкой в этом
случае:

try {
throw new Error('текст исключения');
} catch (error) {
console.log(error.name); // 'Error'
console.log(error.message); // 'текст исключения'
}

Как вы видите, тип нашего исключения — 'Error'.
Такой же тип будет у любых исключений, выброшенных
подобным образом. Это, однако, не всегда
будет удобно, так как, если у нас могут быть
несколько исключений, мы не сможем их отличить
друг от друга.

В JavaScript предусмотрено решение этой проблемы:
можно выбрасывать исключения не только типа
Error, но и любого встроенного в JavaScript
типа ошибки, например, TypeError,
SyntaxError, RangeError.

Давайте для примера выбросим исключение типа
SyntaxError:

try {
throw new SyntaxError('текст исключения');
} catch (error) {
console.log(error.name); // 'SyntaxError'
console.log(error.message); // 'текст исключения'
}

Выбросите исключение с типом TypeError.

Выбросите исключение с типом SyntaxError
и RangeError. Поймайте эти исключения
с помощью одного блока try. В блоке catch
выведите разные сообщения об ошибке для исключений
разных типов.

Свои типы исключений

Количество встроенных в JavaScript исключений
не такое большое и они чаще всего не могут
удовлетворить все наши потребности в разных
типах исключений. Поэтому в JavaScript встроена
возможность создавать исключения со своим типом.

Существуют разные способы сделать это. Самый
простой — в throw передать объект
с ключами name и message:

try {
throw {name: 'MyError', message: 'текст исключения'};
} catch (error) {
console.log(error.name); // 'MyError'
console.log(error.message); // 'текст исключения'
}

Выше я мы сделали функцию, выбрасывающую
исключение при делении на ноль:

function div(a, b) {
if (b !== 0) {
return a / b;
} else {
throw new Error('ошибка деления на ноль');
}
}

Переделайте эту функцию так, чтобы она выбрасывала
исключение с каким-нибудь придуманными нами
типом, например, DivisionByZeroError.

Выше вы делали функцию, выбрасывающую исключение
при попытке извлечь корень из отрицательного
числа. Переделайте вашу функцию так, чтобы
она выбрасывала исключение с придуманным
вами типом. Хорошо подумайте над названием
исключения, чтобы это название было удачным.

Пример применения

Пусть при загрузке страницы сервер создает
HTML код, в котором хранится название, цена
и количество купленного продукта:

<div id="product" data-product="яблоко" data-price="1000" data-amount="5"></div>

Давайте сделаем функцию, которая будет принимать
ссылку на элемент с продуктом и находить
полную стоимость товара (цену умножать на
количество):

function getCost(elem) {
return elem.dataset.price * elem.dataset.amount;
}

Найдем стоимость нашего продукта:

let product = document.querySelector('#product');
let cost = getCost(product);

alert(cost);

Предположим теперь следующую ситуацию: из-за
какого-то сбоя на сервере он прислал нам
товар, в котором отсутствует цена или количество
(или оба сразу), например, вот так:

<div id="product" data-product="яблоко" data-price="1000"></div>

Если теперь попробовать посчитать стоимость
товара, то результате на экран выведется
NaN. Согласитесь, не очень информативно.

Получается, нам нужно как-то обезопасится
от того, что будут отсутствовать нужные нам
атрибуты. Это можно сделать двумя путями.
Первый путь — это сказать, что это нормальное
поведение и просто поверять ифами наличие
нужных нам атрибутов:

function getCost(elem) {
if (elem.dataset.price !== undefined && elem.dataset.amount !== undefined) {
return elem.dataset.price * elem.dataset.amount;
} else {
return 0; // вернем что-нибудь, например, 0 или null или false
}
}

Второй вариант — это сказать, что отсутствие
атрибута data-price или data-amount
— исключительная ситуация. В этом случае
мы будем выбрасывать исключение:

function getCost(elem) {
if (elem.dataset.price !== undefined && elem.dataset.amount !== undefined) {
return elem.dataset.price * elem.dataset.amount;
} else {
throw {
name: 'ProductCostError',
message: 'отсутствует цена или количество у продукта'
};
}
}

Какой из двух вариантов здесь уместнее применить
— это выбор программиста. Он может считать
проблему нормальной работой скрипта или исключительной
ситуацией.

Пусть мы решили, что ситуация исключительная.
Тогда код получения стоимости товара будет
выглядеть вот так:

let product = document.querySelector('#product');

try {
let cost = getCost(product);
alert(cost);
} catch (error) {
// как-то реагируем на исключение
}

Переделайте мой код так, чтобы функция getCost
выбрасывала два типа исключений: если отсутствует
цена и если отсутствует количество. Хорошо
подумайте над названиями этих исключений.
В блоке catch выведите разные сообщения об
ошибке для исключений разных типов.

Еще пример применения

Пусть к нам откуда-то из внешнего мира приходит
JSON с продуктом:

let json = '{"product": "яблоко", "price": 1000, "amount": 5}';
let product = JSON.parse(json);
alert(product.price * product.amount);

Вы уже знаете, что метод JSON.parse будет
выбрасывать исключение, если JSON некорректный.
Давайте поймаем это исключение:

try {
let json = '{"product": "яблоко", "price": 1000, "amount": 5}';
let product = JSON.parse(json);
alert(product.price * product.amount);
} catch (error) {
// как-то реагируем на исключение
}

Однако, может быть такое, что сам по себе
JSON корректный, но не содержит нужных нам
полей, например, нет поля с ценой:

let json = '{"product": "яблоко", "amount": 5}'; // нет цены

Давайте скажем, что это тоже исключительная
ситуация и будем в таком случае выбрасывать
свое пользовательское исключение:

try {
let json = '{"product": "яблоко", "amount": 5}';
let product = JSON.parse(json);

if (product.price !== undefined && product.amount !== undefined) {
alert(product.price * product.amount);
} else {
throw {
name: 'ProductCostError',
message: 'отсутствует цена или количество у продукта'
};
}
} catch (error) {
// как-то реагируем на исключение
}

Теперь блок catch будет получать два типа
исключений: либо JSON вообще некорректен,
и тогда будет исключение типа SyntaxError,
либо JSON корректен, но не содержит нужных
нам полей, и тогда будет исключение типа
ProductCostError.

Давайте в блоке catch будем отлавливать эти
типы исключений:

try {
let json = '{"product": "яблоко", "amount": 5}';
let product = JSON.parse(json);

if (product.price !== undefined && product.amount !== undefined) {
alert(product.price * product.amount);
} else {
throw {
name: 'ProductCostError',
message: 'отсутствует цена или количество у продукта'
};
}
} catch (error) {
if (error.name == 'SyntaxError') {
alert('Некорректный JSON продукта');
} else if (error.name == 'ProductCostError') {
alert('У продукта отсутствует цена или количество');
}
}

Пусть к вам приходит JSON вот такого вида:

let json = `[
{
"name": "user1",
"age": 25,
"salary": 1000
},
{
"name": "user2",
"age": 26,
"salary": 2000
},
{
"name": "user3",
"age": 27,
"salary": 3000
}
]`;

Проверьте этот JSON на общую корректность
при разборе, а после разбора проверьте, что
в результате получается массив, а не что-то
другое. Если в результате получается не массив
— выбросите исключение.

Проброс исключений

Рассмотрим блок catch задачи о JSON продукта:

catch (error) {
if (error.name == 'SyntaxError') {
alert('Некорректный JSON продукта');
} else if (error.name == 'ProductCostError') {
alert('У продукта отсутствует цена или количество');
}
}

Как вы видите, мы ловим два запланированных
нами исключения и как-то реагируем на это.
Но что будет, если возникнет непредусмотренное
нами исключение другого типа? В этом случае
оно тоже попадет в блок catch, но
никакой реакции на это не последует, так
как исключение с другим типом просто не попадет
ни в один из наших ифов.

Когда я говорю, что не будет никакой реакции,
то имею ввиду, что реально никакой: даже
не будет вываливания ошибки в консоль. Наш
код просто молча не будет работать.

Поэтому существует следующее правило: ваш
код должен ловить только те исключения, с
которыми знает, как справится. Если исключение
не известное, то его нужно пробросить
дальше с помощью throw. В этом случае
его выше поймает кто-то более осведомленный
либо исключение вывалится ошибкой в консоль.

Давайте поправим наш код:

catch (error) {
if (error.name == 'SyntaxError') {
alert('Некорректный JSON продукта');
} else if (error.name == 'ProductCostError') {
alert('У продукта отсутствует цена или количество');
} else {
throw error; // пробрасываем исключение далее
}
}

Дан следующий код:

try {
let arr = JSON.parse(json);

for (let i = 0; i < arr.length; i++) {
localStorage.setItem(i, arr[i]);
}
} catch (error) {
if (error.name == 'QuotaExceededError') {
alert('закончилось место в хранилище');
}

if (error.name == 'SyntaxError') {
alert('некорректный JSON');
}
}

Что не так с этим кодом? Исправьте его на
более удачный.

  • Классы и работа с ними
  • Классы, методы и свойства, Class Expression
  • Наследование классов, функция super
  • Статические методы и свойства классов
  • Приватные методы и свойства, оператор instanceof
  • Примеси — mixins
  • Блоки try/catch/finally, оператор throw, проброс исключений
  • Главная
  • JavaScript ООП
  • Классы и работа с ними

В практически любой
язык программирования, в том числе и JavaScript, имеет
специальную конструкцию:

try/catch/finally

позволяющую
«ловить» ошибки во время выполнения программы и предпринимать определенные
действия (обрабатывать эти ошибки). Общая идея работы этого блока заключается в
том, что мы помещаем в блок try потенциально уязвимый скрипт:

let res = 0;
try {
         res = 5/d; 
         console.log(res);
}
catch(error) {
         console.log(error);
}

И, если в нем
что-то пошло не так, то выполнение программы прерывается и управление
передается блоку catch, в котором можно реализовать
определенные действия по обработке ошибки, например, вывести сообщение в
консоль.

Причем, обратите
внимание, если бы в нашем примере отсутствовал блок try, то скрипт
полностью прекратил бы свою работу и его выполнение было бы остановлено
браузером. Но, благодаря наличию этого блока, прерывается только та часть
программы, которая находится в try и, далее, мы уже попадаем в блок catch, чтобы
выполнить определенные действия при нештатной ситуации. И это значительно
повышает надежность работы скрипта.

Если же в блоке try ошибок не
возникло, то блок catch пропускается и не выполняется:

let res = 0;
try {
         let d = 2;
         res = 5/d;
         console.log(res);
}
catch(error) {
         console.log(error);
}

Но, обратите
внимание, блок try выполняется в синхронном режиме, то есть, например,
если в него поместить вот такой асинхронный вызов функции:

let res = 0;
try {
         setTimeout(function() {
                   res = 5/d;
                   console.log(res);
         }, 0);
}
catch(error) {
         console.log(error);
}

То ошибка
обработана не будет, т.к. анонимная функция внутри setTimeout выполняется уже
после выполнения блока try. То есть, блок try сначала
полностью выполнится, а затем, будет запущена анонимная функция. В такой
ситуации блок try/catch следует
поместить внутрь анонимной функции:

let res = 0;
setTimeout(function() {
         try {
                   res = 5/d;
                   console.log(res);
         }
         catch(error) {
                   console.log(error);
         }
}, 0);

Свойства объекта ошибки

В языке JavaScript объект ошибки
блока catch имеет три
полезных атрибута:

  • name
    – содержит
    название ошибки;

  • message
    – содержит
    текст ошибки;

  • stack – стек вызова
    до возникновения ошибки (нестандартное свойство).

Например, мы
можем вывести эти атрибуты в консоль, следующим образом:

let res = 0;
try {
         d = 2;
         res = 5/d;
         console.log(res);
}
catch(error) {
         console.log(error.name);
         console.log(error.message);
         console.log(error.stack);
}

Блок finally

При обработке
ошибок часто используется еще один необязательный блок finally, который
выполняется в любом случае: возникли ошибки или нет, причем, в последнюю
очередь. Зачем он может понадобиться? Например, чтобы выполнить некоторые
обязательные действия после завершения соединения с сервером:

Мы здесь
определяем некоторый флаг (flSend) для определения выполнения
текущего запроса и если он установлен в true, то запрос
второй раз отправляться не будет. Соответственно, потом, после успешной или
ошибочной обработки, этот флаг обязательно нужно вернуть в значение false. Как раз это и
делается в блоке finally.

Генерация собственных исключений – throw

JavaScript позволяет
генерировать свои собственные исключения с помощью оператора

throw
<объект
ошибки>

В качестве
объекта ошибки может быть и число и строка или еще какой-либо примитив и
объект, но лучше использовать стандартные классы ошибок, например:

  • Error
    – для
    общих ошибок;

  • SyntaxError
    – для синтаксических ошибок;

  • TypeError
    – для ошибок типов данных;

  • ReferenceError – ссылка на
    несуществующую переменную

и так далее. Чтобы
воспользоваться этими классами, достаточно создать соответствующий объект:

let err1 = new Error("Ошибка выполнения");
let err2 = new SyntaxError("Ошибка синтаксиса");
let err3 = new TypeError("Ошибка типов данных");

и указать после
оператора throw:

throw err1;
throw new SyntaxError("Ошибка синтаксиса");

Например,
зададим функцию деления двух чисел, которая генерирует исключение, если
знаменатель равен нулю:

function divide(a, b) {
         if( b == 0 ) {
                   throw new Error("Деление на ноль");
         }
 
         return a/b;
}

И, далее,
вызываем ее в блоке try:

let res = 0;
try {
         res = divide(1, 0);
         console.log(res);
}
catch(error) {
         console.log(error.name);
         console.log(error.message);
}

В консоли увидим
строчки:

Error, Деление на
ноль

То есть,
свойство name ссылается на
имя конструктора объекта ошибки, а message – на аргумент
этого объекта.

Проброс исключений

В нашей
последней реализации блок catch отлавливает все исключения в блоке try. Но, что если
мы хотим отслеживать лишь определенный их тип, например, Error, а остальные не
трогать – пересылать дальше – блокам try более высокого уровня:

Это и называется
пробросом исключения. Для его реализации нам достаточно проверить во внутреннем
блоке catch тип ошибки и,
если она не соответствует Error, то передать дальше:

// let res = 0;
try {
         res = divide(1, 2);
         console.log(res);
}
catch(error) {
         if(error.name == "Error") {
                   console.log(error.name);
                   console.log(error.message);
         }
         else {
                   throw error;
         }
}

Соответственно,
внутри будет обрабатывать только исключения Error, а остальные
пробрасывать блокам более высокого уровня.

Видео по теме

  • Предыдущая

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

Здравствуйте!  В этом уроке я хотел бы рассказать об ошибках в JavaScript и о том как их собственно обрабатывать.  Ведь часто случается, что ошибки  происходят  время от времени  и тут даже дело не в наличие опыта программирования или даже  полном его отсутствии. Ведь и у матерых программистов тоже случаются ошибки никто от этого не застрахован.

Ошибки бывают в основном 2-х типов — это синтаксические и логические.  К синтаксическим можно отнести ошибки в имени переменных, функций,  ошибки в синтаксисе кода. В принципе такие ошибки легко отловить через консоль браузера.

А вот логические ошибки с ними все не так просто потому, что они приводят к неправильному выполнению кода программы. Поэтому для их устранения потребуется отладка программы, чтобы понять что собственно происходит на каждом шаге скрипта. Мы же с вами  здесь рассмотрим в основном локализацию синтаксических ошибок с помощью конструкции try…catch.
перехват ошибок в javascript try...catch

Конструкция перехвата ошибок try…catch

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

try {

  // код ...

} catch (err) {

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

}

Работает эта конструкция таким образом:

  1. Выполняется код внутри блока try, так называемой ловушки.
  2. Если в нём не встречаются ошибки, то блок catch(err) игнорируется.
  3. А вот, если в нём возникнет ошибка, то выполнение try будет прервано на ошибке, и управление  передается в начало блока catch(err). При этом переменная err (можно выбрать любое другое название) будет содержать объект ошибки с подробнейшей информацией о произошедшей ошибке.

Поэтому  при ошибке в try скрипт не останавливается, и  даже более того мы имеем возможность обработать ошибку внутри  блока catch.

Рассмотрим это на примерах.

  • Пример без ошибок: при запуске сработают alert (1) и (2):
    try { alert('Блок try'); // (1) <-- // .. код без ошибок alert('Конец try'); // (2) <-- } 
    catch(e) { alert('Блок catch не получит управление, так как нет ошибок'); // (3) } 
    alert("Потом код продолжит выполнение...");

А вот пример с ошибкой: при запуске сработают (1) и (3):

try { alert('Начало try'); // (1) <-- lalala; // ошибка, переменная не определена! alert('Конец блока try'); // (2) } 
catch(e) { alert('Ошибка ' + e.name + ":" + e.message + "n" + e.stack); // (3) <-- } 
alert("Потом код продолжит выполнение...");

В случае, если грубо нарушена структура кода, не закрыта фигурная скобка или где-то стоит лишняя запятая, то вам никакой try..catch не поможет. Это синтаксические ошибки, интерпретатор такой код просто не понимает.

Важное замечание try..catch может работать только в синхронном коде

Ошибку, которая может произойти в коде,который будет выполняться через время например в setTimeout, try..catch не поймает:

try {
  setTimeout(function() {
    throw new Error(); // вылетит в консоль
  }, 1000);
} catch (e) {
  alert( "не сработает" );
}

На момент запуска функции, назначенной через setTimeout, этот код уже завершится, и интерпретатор выйдет из блока try..catch.

Для того чтобы Чтобы поймать ошибку внутри функции из setTimeout, и try..catch должен быть в той же функции.

Объект ошибки

В примере выше мы видим объект ошибки, который вы передаете в блок catch.  У него есть три основных свойства:

name
Тип ошибки. Например, при обращении к переменной, которой нет: «ReferenceError».
message
Сообщение о деталях ошибки
stack
Выводит более полную информацию, с указанием строки, где произошла ошибка и саму ошибку.

Генерация своих ошибок

Оператор throw

Данный оператор throw генерирует ошибку.

Синтаксис: throw <объект ошибки>.

Технически в качестве объекта ошибки вы можете передать что угодно, это может быть даже  и не объект, а число или строка.

В качестве конструктора ошибок  вы можете использовать встроенный конструктор: new Error(message) или любой другой.

В JavaScript также встроен ряд конструкторов для стандартных ошибок: SyntaxError, ReferenceError, RangeError и некоторые другие.

В данном примере мы используем конструктор new SyntaxError(message). Он создаёт ошибку того же типа, что и JSON.parse.

var data1 = '{ "age": 30 }'; // данные неполны
try {
  var user1 = JSON.parse(data); // <-- выполнится без ошибок
  if (!user1.name) {
    throw new SyntaxError("Данные некорректны");
  }
  alert( user1.name );

} catch (e) {
  alert( "Извините, в данных ошибка" );
}

Получается, что блок catch – единственное место для обработки ошибок во всех случаях.

Оборачивание исключений

И, для полноты картины – последняя,  техника по работе с ошибками. Она, является стандартной практикой во многих языках программирования.

Цель функции readData в примере выше – это прочитать данные. При этом чтении могут возникать различные ошибки, не только SyntaxError, но и, возможно, к примеру URIError  и другие.

Код, который вызвает readData, хотел бы иметь конечно либо результат, либо информацию об ошибке.

При этом очень важным является следующий вопрос: обязан ли этот внешний код знать о типах ошибок, которые возникают при чтении данных, и конечно же уметь перехватывать их?

В данном случае, если при чтении данных происходит ошибка, то будем генерировать её в виде объекта ReadError, с сообщением. А «исходную» ошибку на всякий случай тоже сохраним, присвоим в свойство cause.

Выглядит это так:

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, в случае  если они были.

Попробуйте запустить такой код?

try {
  alert( 'try' );
  if (confirm('Сгенерировать ошибку?')) BAD_CODE();
} catch (e) {
  alert( 'catch' );
} finally {
  alert( 'finally' );
}

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

  1. Если вы ответите на вопрос «сгенерировать ошибку?» утвердительно, то try -> catch -> finally.
  2. Если ответите отрицательно, то try -> finally.

Секцию finally используют, чтобы завершить начатые операции при любом варианте развития событий.

Последняя надежда: window.onerror

Допустим, ошибка произошла вне блока try..catch или выпала из try..catch наружу, во внешний код. Скрипт перестал работать.

Можно ли как-то узнать о том, что произошло? Да можно.

В браузере есть специальное свойство window.onerror, если в него записать функцию, то она выполнится и получит в аргументах сообщение ошибки, текущий URL и номер строки, откуда «выпала» ошибка.

Необходимо лишь позаботиться, чтобы функция была назначена заранее.

Например:

<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 для этого используют следующие методы:

  • Конструкция try..catch..finally –  позволяет обработать ошибку в скрипте, как бы локализовать область с ошибкой.
      try { .. попытка выполнить код .. } catch(e) { .. перехват исключения .. } finally { .. выполняется всегда .. }
  • Возможны также  и другие варианты try..catch или try..finally.
  • Для  генерации пользовательской  ошибки используется оператор throw err.

Есть также и другие интересные приемы:

  • Проброс исключения – catch(err) обрабатывает только те ошибки, которые вы хотели бы  в нём увидеть,  а вот остальные – пробрасывать дальше через  throw err. Определить, нужная ли это ошибка, можно, по свойству name.
  • Оборачивание исключений –  эта некая функция, которая позволяет обернуть исключения  и пробросить их дальше.
  • В window.onerror можно присвоить специальную  функцию, которая выполнится при любой ошибке.

Задача

Сравните два фрагмента кода.

  1. Первый использует finally для выполнения кода по выходу из try..catch:
    try {
      начать работу
      работать
    } catch (e) {
      обработать ошибку
    } finally {
      финализация: завершить работу
    }

Второй фрагмент просто ставит очистку ресурсов за try..catch:

try {
  начать работу
} catch (e) {
  обработать ошибку
}

финализация: завершить работу

Нужно, чтобы код финализации всегда выполнялся при выходе из блока try..catch и, таким образом, заканчивал начатую работу. Имеет ли здесь finally какое-то преимущество или оба фрагмента работают одинаково?

Если вы нашли ошибку, пожалуйста, выделите фрагмент текста и нажмите Ctrl+Enter.

Поделиться

Твитнуть

Поделиться

(Visited 344 times, 1 visits today)

Понравилась статья? Поделить с друзьями:
  • Проблемы со звуком на компьютере как исправить
  • Проблемы при сканировании частоты триколор как исправить проблему
  • Проблемы при сканировании частот триколор как исправить
  • Проблемы врачебной ошибки ятрогенные заболевания
  • Проблема универсальный аудио драйвер для windows 10 как исправить