Обработка ошибок в 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