Тонкости nodejs. Часть II: Работа c ошибками

в 14:00, , рубрики: error, node.js

Обработка ошибок в 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. Но используются они по разному, поэтому рекомендую использовать для этого следующие правила:

  1. Поле name должно содержать значение в "скачущем" регистре: MyError.
  2. Поле code должно содержать значение разделенное подчеркиванием, символы должны быть в верхнем регистре: SOMETHING_WRONG.
  3. Не используйте в поле code слово ERROR.
  4. Значение в name создано для калссификации ошибок, поэтому лучше использовать ConnectionError либо MongoError, вместо MongoConnectionError.
  5. Значение code должно быть уникальным.
  6. Поле message должно формироваться на основе значения code и переданных переменных параметров.
  7. Для успешной обработки ошибки желательно добавить дополнительные сведения в сам объект.
  8. Дополнительные значения должны быть примитивами: не стоит передавать в объект ошибки соединение с базой данных.

Пример:

Чтобы создать отчет об ошибке чтения файла по причине того что файл отсутствует можно указать следующие значения: 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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js