Обработка ошибок в JS – та еще головная боль. Не ошибусь, если скажу, что ошибки – самое слабое место всего языка. При чем проблема состоит из двух других: сложности отлова ошибки в асинхронном коде и плохо спроектированного объекта Error. И если первой проблеме посвящено множество статей, то о второй многие незаслуженно забывают. В этом материале я постараюсь восполнить недостаток и рассмотреть объект Error более пристально.
Корень зла
Реализация объекта ошибки в JS – одна из самых ужасных, которые я когда либо встречал. Мало того сама реализация отличается в различных движках. Объект спроектирован (и развивается) так будто ни до, ни после возникновения JS с ошибками вообще не работали. Я даже не знаю с чего начать. Этот объект не является программно-интерпретируемым, так как все важные значения являются склеенными строками. Отсутствует механизм захвата стека вызовов и алгоритм расширения ошибки.
Результатом этого является то, что каждый разработчик вынужден самостоятельно принимать решение в каждом отдельном случае, но, как доказали ученые, выбор вызывает у людей дискомфорт, поэтому очень часто ошибки просто-напросто игнорируются и попадают в основной поток. Так же, достаточно часто, вместо ошибки вы можете получить Array или Object, спроектированные «на свое усмотрение». Поэтому вместо единой системы обработки ошибок мы сталкиваемся с набором уникальных правил для каждого отдельного случая.
И это не только мои слова, тот же TJ Holowaychuck написал об этом в своем письме прощаясь с сообществом nodejs.
Как же решить проблему? Создать единую стратегию формирования и обработки сообщения об ошибке! Разработчики Google предлагают пользователям V8 собственный набор инструментов, которые облегчают эту задачу. И так приступим.
MyError
Давайте начнем с создания собственного объекта ошибки. В классической теории, все что вы можете сделать – создать экземпляр Error, а затем дополнить его, вот как это выглядит:
var error = new Error('Some error');
error.name = 'My Error';
error.customProperty = 'some value';
throw error;
И так для каждого случая? Да! Конечно, можно было бы создать конструктор MyError и в нем установить нужные значения полей:
function MyError(message, customProperty) {
Error.call(this);
this.message = message;
this.customProperty = customProperty;
}
Но так мы получим в стеке лишнюю запись об ошибке, что усложнит поиск ошибки другим разработчикам. Решением является метод Error.captureStackTrace. Он получает на вход два значения: объект, в который будет записан стек и функция-конструктор, запись о которой из стека нужно изъять.
function MyError(message, customProperty) {
Error.captureStackTrace(this, this.constructor);
this.message = message;
this.customProperty = customProperty;
}
// Для успешного сравнения с помощью ...instanceof Error:
var inherits = require('util').inherits;
inherits(MyError, Error);
Теперь где бы не всплыла ошибка в стеке на первом месте будет стоять адрес вызова new Error.
message, name и code
Следующим пунктом в решении проблемы стоит идентификация ошибки. Для того чтобы программно ее обработать и принять решение о дальнейших действиях: выдать пользователю сообщение или завершить работу. Поле message не дает таких возможностей: парсить сообщение регулярным выражением не представляется разумным. Как же тогда отличить ошибку неверного параметра от ошибки соединения? В самом nodejs для этого используется поле code. При этом в стандарте для классификации ошибок предписывается использовать поле name. Но используются они по разному, поэтому рекомендую использовать для этого следующие правила:
- Поле name должно содержать значение в «скачущем» регистре:
MyError
. - Поле code должно содержать значение разделенное подчеркиванием, символы должны быть в верхнем регистре:
SOMETHING_WRONG
. - Не используйте в поле code слово
ERROR
. - Значение в name создано для классификация ошибок, поэтому лучше использовать
ConnectionError
либоMongoError
, вместоMongoConnectionError
. - Значение code должно быть уникальным.
- Поле message должно формироваться на основе значения code и переданных переменных параметров.
- Для успешной обработки ошибки желательно добавить дополнительные сведения в сам объект.
- Дополнительные значения должны быть примитивами: не стоит передавать в объект ошибки соединение с базой данных.
Пример:
Чтобы создать отчет об ошибке чтения файла по причине того что файл отсутствует можно указать следующие значения: FileSystemError
для name
и FILE_NOT_FOUND
для code
, а также к ошибке следует добавить поле file
.
Обработка стека
Так же в V8 есть функция Error.prepareStackTrace
для получения сырого стека – массива CallSite объектов. CallSite – это объекты, которые содержат информацию о вызове: адрес ошибки (метод, файл, строка) и ссылки непосредственно на сами объекты чьи методы были вызваны. Таким образом в наших руках оказывается достаточно мощный и гибкий инструмент для дебага приложений.
Для того чтобы получить стек необходимо создать функцию, которая на вход получает два аргумента: непосредственно ошибка и массив CallSite объектов, вернуть необходимо готовую строку. Эта функция будет вызываться для каждой ошибок, при обращении к полю stack. Созданную функцию необходимо добавить в сам Error как prepareStackTrace:
Error.prepareStackTrace = function(error, stack) {
// ...
return error + ':n' + stackAsString;
};
Давайте подробнее рассмотрим объект CallSite содержащийся в массиве stack. Он имеет следующие методы:
Метод | Описание |
---|---|
getThis | возвращает значение this. |
getTypeName | возвращает тип this в виде строки, обычно это поле name конструктора. |
getFunction | возвращает функцию. |
getFunctionName | возвращает имя функции, обычно это значение поля name. |
getMethodName | возвращает имя поля объекта this. |
getFileName | возвращает имя файла (или скрипта для браузера). |
getLineNumber | возвращает номер строки. |
getColumnNumber | возвращает смещение в строке. |
getEvalOrigin | возвращает место вызова eval, если функция была объявлена внутри вызова eval. |
isTopLevel | является ли вызов вызовом из глобальной области видимости. |
isEval | является ли вызов вызовом из eval. |
isNative | является ли вызваный метод внутренним. |
isConstructor | является ли метод вызовом конструктора. |
Как я уже говорил выше этот метод будет вызываться один раз и для каждой ошибки. При этом вызов будет происходить только при обращении к полю stack. Как это использовать? Можно внутри метода добавить к ошибке стек в виде массива:
Error.prepareStackTrace = function(error, stack) {
error._stackAsArray = stack.map(function(call){
return {
// ...
file : call.getFileName()
};
});
// ...
return error + ':n' + stackAsString;
};
А затем в саму ошибку добавить динамическое свойство для получение стека.
Object.defineProperty(MyError.prototype, 'stackAsArray', {
get : function() {
// Инициируем вызов prepareStackTrace
this.stack;
return this._stackAsArray;
}
});
Так мы получили полноценный отчет, который доступен программно и позволяет отделить системные вызовы от вызовов модулей и от вызовов самого приложения для подробного анализа и обработки. Сразу оговорюсь, что тонкостей и вопросов при анализе стека может возникнуть очень много, поэтому, если хотите разобраться, советую покопаться самостоятельно.
Все изменения в API следует отслеживать на wiki-странице v8 посвященной ErrorTraceAPI.
Заключение
На этом хочу закончить, наверное для вводной статьи этого достаточно. Ну и надеюсь, что кому-то этот материал спасет время и нервы в будущем. В следующей статье я расскажу, как сделать работу с ошибками комфортной с помощью описанных в статье подходов и инструментов.
UPD. Всем кто ждет откровений про отлов асинхронных ошибок: to be continued…
Оператор try
позволяет вам проверить блок кода на наличие ошибок.
Оператор catch
позволяет вам обработать ошибку.
Оператор throw
позволяет создавать собственные ошибки.
Оператор finally
позволяет выполнить код, после того, как попытаться поймать, независимо от результата.
Ошибки будут!
При выполнении кода JavaScript могут возникать разные ошибки.
Ошибки могут быть ошибками кодирования, допущенны программистом, ошибками из-за неправильного ввода или другими непредвиденными ситуациями.
Пример
В этом примере мы в место alert написали adddlert, чтобы намеренно выдать ошибку:
<p id=»demo»></p>
<script>
try {
adddlert(«Добро пожаловать!»);
}
catch(err) {
document.getElementById(«demo»).innerHTML = err.message;
}
</script>
Попробуйте сами »
JavaScript перехватывает adddlert, как ошибку и выполняет код перехвата для ее обработки.
JavaScript try и catch
Оператор try
, позволяет обнаружить блок кода, который будет проверяться на наличие ошибок во время его выполнения.
Оператор catch
, позволяет поймать блок кода, который будет выполняться, если в блоке try возникает ошибка.
Оператор JavaScript try
и catch
идут в парах:
try {
Блок кода для проверки
}
catch(err) {
Блок кода для обработки ошибок
}
JavaScript выдает ошибки
Когда происходит ошибка, JavaScript обычно останавливается и генерирует сообщение об ошибке.
Технический термин для этого: JavaScript вызовет исключение (выдаст ошибку).
JavaScript фактически создаст объект Error с двумя свойствами: name и message.
Оператор throw
Оператор throw
позволяет создать пользовательскую ошибку.
Технически вы можете сгенерировать исключение (сгенерировать ошибку).
Исключением может быть JavaScript String
, Number
, Boolean
или Object
:
throw «Слишком большой»; // пропустить текст
throw 500; // пропустить число
Если вы используете throw
вместе с try
и catch
, вы можете контролировать выполнение программы и генерировать собственные сообщения об ошибках.
Пример проверки ввода
В этом примере исследуется ввод. Если значение неверно, генерируется исключение (ошибка).
Исключение (err) перехватывается оператором catch, и отображается настраиваемое сообщение об ошибке:
<!DOCTYPE html>
<html>
<body>
<p>Пожалуйста, введите число от 5 до 10:</p>
<input id=»demo» type=»text»>
<button type=»button»
onclick=»myFunction()»>Ввод текста</button>
<p id=»p01″></p>
<script>
function myFunction() {
var message, x;
message =
document.getElementById(«p01»);
message.innerHTML = «»;
x =
document.getElementById(«demo»).value;
try {
if(x == «») throw «пусто»;
if(isNaN(x)) throw «не число»;
x = Number(x);
if(x < 5) throw
«слишком маленькое»;
if(x > 10) throw «слишком
большое»;
}
catch(err) {
message.innerHTML =
«Вывод » + err;
}
}
</script>
</body>
</html>
Попробуйте сами »
Проверка HTML на валидность
Код выше — является просто примером.
Современные браузеры часто используют комбинацию JavaScript и встроенной проверки HTML, используя предопределенные правила проверки, определенные в атрибутах HTML:
<input id=»demo» type=»number» min=»5″ max=»10″ step=»1″>
Вы можете узнать больше о проверке форм в следующей главе этого руководства.
Оператор finally
Оператор finally
позволяет выполнить код, после try и catch, еще раз попытаться поймать, независимо от результата:
Синтаксис
try {
блок кода для попытки обнаружить ошибки
}
catch(err) {
блок кода для обработки поймать ошибки
}
finally {
блок кода, который будет выполняться независимо от результата try/catch
}
Пример
function myFunction() {
var message, x;
message =
document.getElementById(«p01»);
message.innerHTML = «»;
x =
document.getElementById(«demo»).value;
try {
if(x == «») throw «пусто»;
if(isNaN(x))
throw «не число»;
x = Number(x);
if(x >
10) throw «слишком большое»;
if(x <
5) throw «слишком маленькое»;
}
catch(err)
{
message.innerHTML = «Ошибка: » +
err + «.»;
}
finally {
document.getElementById(«demo»).value = «»;
}
}
Попробуйте сами »
Объект Error
JavaScript имеет встроенный объект Error, который предоставляет информацию об ошибке при возникновении ошибки.
Объект Error предоставляет два полезных свойства: имя и сообщение.
Свойства объекта Error
Свойство | Описание |
---|---|
имя | Задает или возвращает имя ошибки |
сообщение | Устанавливает или возвращает сообщение об ошибке (строку) |
Значения Error Name
Шесть различных значений могут быть возвращены свойством Error Name:
Error Name | Описание |
---|---|
EvalError | Произошла ошибка в функции eval() |
RangeError | Произошло число «вне допустимого диапазона» |
ReferenceError | Произошла недопустимая ссылка |
SyntaxError | Произошла синтаксическая ошибка |
TypeError | Произошла ошибка типа |
URIError | Произошла ошибка в encodeURI() |
Ниже описаны шесть различных значений.
Ошибка EvalError
Один EvalError
указывает на ошибку в функции eval().
Новые версии JavaScript не генерируют EvalError. Вместо этого используйте SyntaxError.
Ошибка RangeError
Ошибка RangeError
генерируется, если вы используете число, выходящее за пределы диапазона допустимых значений.
Например: Вы не можете установить количество значащих цифр числа на 500.
Пример
var num = 1;
try {
num.toPrecision(500); // Число не может содержать 500 значащих цифр
}
catch(err) {
document.getElementById(«demo»).innerHTML = err.name;
}
Попробуйте сами »
Ошибка ReferenceError
Ошибка ReferenceError
генерируется, если вы используете (ссылаетесь на неё) переменную,
которая не была объявлена:
Пример
var x;
try {
x = y + 1; // на y нельзя ссылаться (использовать)
}
catch(err) {
document.getElementById(«demo»).innerHTML = err.name;
}
Попробуйте сами »
Ошибка SyntaxError
Ошибка SyntaxError
генерируется, если вы пытаетесь запустить код с синтаксической ошибкой.
Пример
try {
eval(«alert(‘Привет)»); // Отсутствует ‘вызовет ошибку
}
catch(err) {
document.getElementById(«demo»).innerHTML = err.name;
}
Попробуйте сами »
Ошибка TypeError
Ошибка TypeError
генерируется, если вы используете значение, которое находится за пределами диапазона ожидаемых типов:
Пример
var num = 1;
try {
num.toUpperCase(); // Вы не можете преобразовать число в верхний регистр
}
catch(err) {
document.getElementById(«demo»).innerHTML = err.name;
}
Попробуйте сами »
Ошибка URIError (Единый идентификатор ресурса)
Ошибка URIError
генерируется, если вы используете недопустимые символы в функции URI:
Пример
try {
decodeURI(«%%%»); // Вы не можете декодировать знаки процента URI
}
catch(err) {
document.getElementById(«demo»).innerHTML = err.name;
}
Попробуйте сами »
Нестандартные свойства объекта ошибок
Mozilla и Microsoft определяют некоторые нестандартные свойства объекта ошибки:
fileName (Mozilla)
lineNumber (Mozilla)
columnNumber (Mozilla)
stack (Mozilla)
description (Microsoft)
number (Microsoft)
Не используйте эти свойства на общедоступных веб-сайтах. Они не будут работать во всех браузерах.
Полный справочник ошибок
Чтобы получить полную информацию об объекте Error, перейдите в Полный справочник ошибок JavaScript ..
Обработка ошибок в JS – та еще головная боль. Не ошибусь, если скажу, что ошибки – самое слабое место всего языка. При чем проблема эта составная и состоит она из двух других проблем: сложность отлова ошибки в асинхронном коде и плохо спроектированный объект Error. Вторая, на самом, деле менее очевидная, поэтому с нее и начнем.
Корень зла
Реализация объекта ошибки в JS – одна из самых ужасных, которые я когда либо встречал. Мало того сама реализация отличается в различных движках. Объект спроектирован (и развивается) так будто ни до, ни после возникновения JS с ошибками вообще не работали. Я даже не знаю с чего начать. Этот объект не является программно-интерпретируемым, так как все важные значения являются склеенными строками. Отсутствует механизм захвата стека вызовов и алгоритм расширения ошибки.
Результатом этого является то, что каждый разработчик вынужден самостоятельно принимать решение в каждом отдельном случае, но, как доказали ученые, выбор вызывает у людей дискомфорт, поэтому очень часто ошибки просто-напросто игнорируются и попадают в основной поток. Так же, достаточно часто, вместо ошибки вы можете получить Array или Object, спроектированные «на свое усмотрение». Поэтому вместо единой системы обработки ошибок мы сталкиваемся с набором уникальных правил для каждого отдельного случая.
И это не только мои слова, тот же TJ Holowaychuck написал об этом в своем письме прощаясь с сообществом nodejs.
Как же решить проблему? Создать единую стратегию формирования и обработки сообщения об ошибке! Разработчики Google предлагают пользователям V8 собственный набор инструментов, которые облегчают эту задачу. И так приступим.
MyError
Давайте начнем с создания собственного объекта ошибки. В классической теории, все что вы можете сделать – создать экземпляр Error, а затем дополнить его, вот как это выглядит:
var error = new Error('Some error');
error.name = 'My Error';
error.customProperty = 'some value';
throw error;
И так для каждого случая? Да! Конечно, можно было бы создать конструктор MyError и в нем установить нужные значения полей:
function MyError(message, customProperty) {
Error.call(this);
this.message = message;
this.customProperty = customProperty;
}
Но так мы получим в стеке лишнюю запись об ошибке, что усложнит поиск ошибки другим разработчикам. Решением является метод Error.captureStackTrace. Он получает на вход два значения: объект, в который будет записан стек и функция-конструктор, запись о которой из стека нужно изъять.
function MyError(message, customProperty) {
Error.captureStackTrace(this, this.constructor);
this.message = message;
this.customProperty = customProperty;
}
// Для успешного сравнения с помощью ...instanceof Error:
var inherits = require('util').inherits;
inherits(MyError, Error);
Теперь где бы не всплыла ошибка в стеке на первом месте будет стоять адрес вызова new Error.
message, name и code
Следующим пунктом в решении проблемы стоит идентификация ошибки. Для того чтобы программно ее обработать и принять решение о дальнейших действиях: выдать пользователю сообщение или завершить работу. Поле message не дает таких возможностей: парсить сообщение регулярным выражением не представляется разумным. Как же тогда отличить ошибку неверного параметра от ошибки соединения? В самом nodejs для этого используется поле code. При этом в стандарте для классификации ошибок предписывается использовать поле name. Но используются они по разному, поэтому рекомендую использовать для этого следующие правила:
- Поле name должно содержать значение в «скачущем» регистре:
MyError
. - Поле code должно содержать значение разделенное подчеркиванием, символы должны быть в верхнем регистре:
SOMETHING_WRONG
. - Не используйте в поле code слово
ERROR
. - Значение в name создано для калссификации ошибок, поэтому лучше использовать
ConnectionError
либоMongoError
, вместоMongoConnectionError
. - Значение code должно быть уникальным.
- Поле message должно формироваться на основе значения code и переданных переменных параметров.
- Для успешной обработки ошибки желательно добавить дополнительные сведения в сам объект.
- Дополнительные значения должны быть примитивами: не стоит передавать в объект ошибки соединение с базой данных.
Пример:
Чтобы создать отчет об ошибке чтения файла по причине того что файл отсутствует можно указать следующие значения: FileSystemError
для name
и FILE_NOT_FOUND
для code
, а также к ошибке следует добавить поле file
.
Обработка стека
Так же в V8 есть функция Error.prepareStackTrace
для получения сырого стека – массива CallSite объектов. CallSite – это объекты, которые содержат информацию о вызове: адрес ошибки (метод, файл, строка) и ссылки непосредственно на сами объекты чьи методы были вызваны. Таким образом в наших руках оказывается достаточно мощный и гибкий инструмент для дебага приложений.
Для того чтобы получить стек необходимо создать функцию, которая на вход получает два аргумента: непосредственно ошибка и массив CallSite объектов, вернуть необходимо готовую строку. Эта функция будет вызываться для каждой ошибок, при обращении к полю stack. Созданную функцию необходимо добавить в сам Error как prepareStackTrace:
Error.prepareStackTrace = function(error, stack) {
// ...
return error + ':n' + stackAsString;
};
Давайте подробнее рассмотрим объект CallSite содержащийся в массиве stack. Он имеет следующие методы:
getThis | возвращает значение this. |
getTypeName | возвращает тип this в виде строки, обычно это поле name конструктора. |
getFunction | возвращает функцию. |
getFunctionName | возвращает имя функции, обычно это значение поля name. |
getMethodName | возвращает имя поля объекта this. |
getFileName | возвращает имя файла (или скрипта для браузера). |
getLineNumber | возвращает номер строки. |
getColumnNumber | возвращает смещение в строке. |
getEvalOrigin | возвращает место вызова eval, если функция была объявлена внутри вызова eval. |
isTopLevel | является ли вызов вызовом из глобальной области видимости. |
isEval | является ли вызов вызовом из eval. |
isNative | является ли вызваный метод внутренним. |
isConstructor | является ли метод вызовом конструктора. |
Как я уже говорил выше этот метод будет вызываться один раз и для каждой ошибки. При этом вызов будет происходить только при обращении к полю stack. Как это использовать? Можно внутри метода добавить к ошибке стек в виде массива:
Error.prepareStackTrace = function(error, stack) {
error._stackAsArray = stack.map(function(call){
return {
// ...
file : call.getFileName()
};
});
// ...
return error + ':n' + stackAsString;
};
А затем в саму ошибку добавить динамическое свойство для получение стека.
Object.defineProperty(MyError.prototype, 'stackAsArray', {
get : function() {
// Инициируем вызов prepareStackTrace
this.stack;
return this._stackAsArray;
}
});
Так мы получили полноценный отчет, который доступен программно и позволяет отделить системные вызовы от вызовов модулей и от вызовов самого приложения для подробного анализа и обработки. Сразу оговорюсь, что тонкостей и вопросов при анализе стека может возникнуть очень много, поэтому, если хотите разобраться, советую покопаться самостоятельно.
Все изменения в API следует отслеживать на wiki-странице v8 посвященной ErrorTraceAPI.
Заключение
На этом хочу закончить, наверное для вводной статьи этого достаточно. Ну и надеюсь, что кому-то этот материал спасет время и нервы в будущем.
В следующей статье я расскажу, как сделать работу с ошибками комфортной с помощью описаных в статье подходов и инструментов.
Автор: rumkin
Источник
Данная статья является переводом. Ссылка на оригинал.
В статье рассмотрим:
- Объект Error
- Try…catch
- Throw
- Call stack
- Наименование функций
- Парадигму асинхронного программирования Promise
Представьте, как разрабатываете RESTful web API на Node.js.
- Пользователи отправляют запросы к серверу для получения данных.
- Вопрос времени, когда в программу придут значения, которые не ожидались.
- В таком случае, когда программа получит эти значения, пользователи будут рады видеть подробное и конкретное описание ошибки.
- В случае когда нет соответствующего обработчика ошибки, отобразится стандартное сообщение об ошибке. Таким образом, пользователь увидит сообщение об ошибке «Невозможно выполнить запрос», что не несет полезной информации о том, что произошло.
- Поэтому стоит уделять внимание обработке ошибок, чтобы наша программа стала безопасной, отказоустойчивой, высокопроизводительной и без ошибок.
Анатомия Объекта Error
Первое, с чего стоит начать изучение – это объект Error.
Разберем на примере:
throw new Error('database failed to connect');
Здесь происходят две вещи: создается объект Error
и выбрасывается исключение.
Начнем с рассмотрения объекта Error
, и того, как он работает. К ключевому слову throw
вернемся чуть позже.
Объект Error
представляет из себя реализацию функции конструктора, которая использует набор инструкций (аргументы и само тело конструктора) для создания объекта.
Тем не менее, что же такое объекты ошибок? Почему они должны быть однородными? Это важные вопросы, поэтому давайте перейдем к ним.
Первым аргументом для объекта Error
является его описание.
Описание – это понятная человеку строка объекта ошибки. Также эта строка появляется в консоли, когда что-то пошло не так.
Объекты ошибок также имеют свойство name
, которое рассказывает о типе ошибки. Когда создается нативный объект ошибки, то свойство name
по умолчанию содержит Error. Вы также можете создать собственный тип ошибки, расширив нативный объект ошибки следующим образом:
class FancyError extends Error {
constructor(args){
super(args);
this.name = "FancyError"
}
}
console.log(new Error('A standard error'))
// { [Error: A standard error] }
console.log(new FancyError('An augmented error'))
// { [Your fancy error: An augmented error] name: 'FancyError' }
Обработка ошибок становится проще, когда у нас есть согласованность в объектах.
Ранее мы упоминали, что хотим, чтобы объекты ошибок были однородными. Это поможет обеспечить согласованность в объекте ошибки.
Теперь давайте поговорим о следующей части головоломки – throw
.
Ключевое слово Throw
Создание объектов ошибок – это не конец истории, а только подготовка ошибки к отправке. Отправка ошибки заключается в том, чтобы выбросить исключение. Но что значит выбросить? И что это значит для нашей программы?
Throw
делает две вещи: останавливает выполнение программы и находит зацепку, которая мешает выполнению программы.
Давайте рассмотрим эти идеи одну за другой:
- Когда JavaScript находит ключевое слово
throw
, первое, что он делает – предотвращает запуск любых других функций. Остановка снижает риск возникновения любых дальнейших ошибок и облегчает отладку программ. - Когда программа остановлена, JavaScript начнет отслеживать последовательную цепочку функций, которые были вызваны для достижения оператора
catch
. Такая цепочка называется стек вызовов (англ. call stack). Ближайшийcatch
, который находит JavaScript, является местом, где возникает выброшенное исключение. Если операторыtry/catch
не найдены, тогда возникает исключение, и процесс Node.js завершиться, что приведет к перезапуску сервера.
Бросаем исключения на примере
Мы рассмотрели теорию, а теперь давайте изучим пример:
function doAthing() {
byDoingSomethingElse();
}
function byDoingSomethingElse() {
throw new Error('Uh oh!');
}
function init() {
try {
doAthing();
} catch(e) {
console.log(e);
// [Error: Uh oh!]
}
}
init();
Здесь в функции инициализации init()
предусмотрена обработка ошибок, поскольку она содержит try/catch
блок.
init()
вызывает функцию doAthing()
, которая вызывает функцию byDoingSomethingElse()
, где выбрасывается исключение. Именно в этот момент ошибки, программа останавливается и начинает отслеживать функцию, вызвавшую ошибку. Далее в функции init()
и выполняет оператор catch
. С помощью оператора catch
мы решаем что делать: подавить ошибку или даже выдать другую ошибку (для распространения вверх).
Стек вызовов
То, что показано в приведенном выше примере – это проработанный пример стека вызовов. Как и большинство языков, JavaScript использует концепцию, известную как стек вызовов.
Но как работает стек вызовов?
Всякий раз, когда вызывается функция, она помещается в стек, а при завершении удаляется из стека. Именно от этого стека мы получили название «трассировки стека».
Трассировка стека – это список функций, которые были вызваны до момента, когда в программе произошло исключение.
Она часто выглядит так:
Error: Uh oh!
at byDoingSomethingElse (/filesystem/aProgram.js:7:11)
at doAthing (/filesystem/aProgram.js:3:5)
at init (/filesystem/aProgram.js:12:9)
at Object.<anonymous> (/filesystem/aProgram.js:19:1)
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)
На этом этапе вам может быть интересно, как стек вызовов помогает нам с обработкой ошибок Node.js. Давайте поговорим о важности стеков вызовов.
Стек вызовов предоставляет «хлебные крошки», помогая проследить путь, который привел к исключению(ошибке).
Почему у нас должны быть функции без имен? Иногда в наших программах мы хотим определить маленькие одноразовые функции, которые выполняют небольшую задачу. Мы не хотим утруждать себя задачей давать им имена, но именно эти анонимные функции могут вызвать у нас всевозможные головные боли. Анонимная функция удаляет имя функции из нашего стека вызовов, что делает наш стек вызовов значительно более сложным в использовании.
Обратите внимание, что присвоить имена функциям в JavaScript не так просто. Итак, давайте кратко рассмотрим различные способы определения функций, и рассмотрим некоторые ловушки в именовании функций.
Как называть функции
Чтобы понять, как называть функции, давайте рассмотрим несколько примеров:
// анонимная функция
const one = () => {};
// анонимная функция
const two = function () {};
// функция с явным названием
const three = function explicitFunction() {};
Вот три примера функций.
Первая – это лямбда (или стрелочная функция). Лямбда функции по своей природе анонимны. Не запутайтесь. Имя переменной one
не является именем функции. Имя функции следующее за ключевым словом function
необязательно. Но в этом примере мы вообще ничего не передаем, поэтому наша функция анонимна.
Примечание
Не помогает и то, что некоторые среды выполнения JavaScript, такие как V8, могут иногда угадывать имя вашей функции. Это происходит, даже если вы его не даете.
Во втором примере мы получили функциональное выражение. Это очень похоже на первый пример. Это анонимная функция, но просто объявленная с помощью ключевого слова function
вместо синтаксиса жирной стрелки.
В последнем примере объявление переменной с подходящим именем explicitFunction
. Это показывает, что это единственная функция, у которой соответствующее имя.
Как правило, рекомендуется указывать это имя везде, где это возможно, чтобы иметь более удобочитаемую трассировку стека.
Обработка асинхронных исключений
Мы познакомились с объектом ошибок, ключевым словом throw
, стеком вызовов и наименованием функций. Итак, давайте обратим наше внимание на любопытный случай обработки асинхронных ошибок. Почему? Потому что асинхронный код ведет себя не так, как ожидаем. Асинхронное программирование необходимо каждому программисту на Node.js.
Javascript – это однопоточный язык программирования, а это значит, что Javascript запускается с использованием одного процессора. Из этого следует, что у нас есть блокирующий и неблокирующий код. Блокирующий код относится к тому, будет ли ваша программа ожидать завершения асинхронной задачи, прежде чем делать что-либо еще. В то время как неблокирующий код относится к тому, где вы регистрируете обратный вызов (callback) для выполнения после завершения задачи.
Стоит упомянуть, что есть два основных способа обработки асинхронности в JavaScript: promises (обещания или промисы) и callback (функция обратного вызова). Мы намеренно игнорируем async/wait
, чтобы избежать путаницы, потому что это просто сахар поверх промисов.
В статье мы сфокусируемся на промисах. Существует консенсус в отношении того, что для приложений промисы превосходят обратные вызовы с точки зрения стиля программирования и эффективности. Поэтому в этой статье проигнорируем подход с callback-ами, и предположим, что вместо него вы выберете promises.
Примечание
Существует множество способов конвертировать код на основе callback-ов в promises. Например, вы можете использовать такую утилиту, как promisify, или обернуть свои обратные вызовы в промисы, например, так:
var request = require('request'); //http wrapped module
function requestWrapper(url, callback) {
request.get(url, function (err, response) {
if (err) {
callback(err);
} else {
callback(null, response);
}
})
}
Мы разберемся с этой ошибкой, обещаю!
Давайте взглянем на анатомию обещаний.
Промисы в JavaScript – это объект, представляющий будущее значение. Promise API позволяют нам моделировать асинхронный код так же, как и синхронный. Также стоит отметить, что обещание обычно идет в цепочке, где выполняется одно действие, затем другое и так далее.
Но что все это значит для обработки ошибок Node.js?
Промисы элегантно обрабатывают ошибки и перехватывают любые ошибки, которые им предшествовали в цепочке. С помощью одного обработчика обрабатывается множество ошибок во многих функциях.
Изучим код ниже:
function getData() {
return Promise.resolve('Do some stuff');
}
function changeDataFormat() {
// ...
}
function storeData(){
// ...
}
getData()
.then(changeDataFormat)
.then(storeData)
.catch((e) => {
// Handle the error!
})
Здесь видно, как объединить обработку ошибок для трех различных функций в один обработчик, т. е. код ведет себя так же, как если бы три функции заключались в синхронный блок try/catch
.
Отлавливать или не отлавливать?
На данном этапе стоит спросить себя, повсеместно ли добавляется .catch
к промисам, поскольку это опционально. Из-за проблем с сетью, аппаратного сбоя или истекшего времени ожидания в асинхронных вызовах возникает исключение. По этим причинам указывайте программе, что делать в случаях невыполнения промиса.
Запомните «Золотое правило» – каждый раз обрабатывать исключения в обещаниях.
Риски асинхронного try/catch
Мы приближаемся к концу в нашем путешествии по обработке ошибок в Node.js. Пришло время поговорить о ловушках асинхронного кода и оператора try/catch
.
Вам может быть интересно, почему промис предоставляет метод catch
, и почему мы не можем просто обернуть нашу реализацию промиса в try/catch
. Если бы вы сделали это, то результаты были бы не такими, как вы ожидаете.
Рассмотрим на примере:
try {
throw new Error();
} catch(e) {
console.log(e); // [Error]
}
try {
setTimeout(() => {
throw new Error();
}, 0);
} catch(e) {
console.log(e); // Nothing, nada, zero, zilch, not even a sound
}
try/catch
по умолчанию синхронны, что означает, что если асинхронная функция выдает ошибку в синхронном блоке try/catch
, ошибка не будет брошена.
Однозначно это не то, что ожидаем.
***
Подведем итог! Необходимо использовать обработчик промисов, когда мы имеем дело с асинхронным кодом, а в случае с синхронным кодом подойдет try/catch
.
Заключение
Из этой статьи мы узнали:
- как устроен объект Error;
- научились создавать свои собственные ошибки;
- как работает стек вызовов;
- практики наименования функций, для удобочитаемой трассировки стека;
- как обрабатывать асинхронные исключения.
***
Материалы по теме
- 🗄️ 4 базовых функции для работы с файлами в Node.js
- Цикл событий: как выполняется асинхронный JavaScript-код в Node.js
- Обработка миллионов строк данных потоками на Node.js