Обработка исключений, сброшенные ошибки, в пределах promises

Я запускаю внешний код в качестве стороннего расширения для службы node.js. Методы API возвращают promises. Решенное обещание означает, что действие было выполнено успешно, неудачное обещание означает, что была какая-то проблема, выполняющая операцию.

Теперь, когда у меня возникают проблемы.

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

Однако, поскольку весь код завершается в promises, эти заброшенные исключения на самом деле возвращаются как неудачные promises.

Я попытался поместить вызов функции в блок try/catch, но он никогда не запускался:

// worker process
var mod = require('./3rdparty/module.js');
try {
  mod.run().then(function (data) {
    sendToClient(true, data);
  }, function (err) {
    sendToClient(false, err);
  });
} catch (e) {
  // unrecoverable error inside of module
  // ... send signal to restart this worker process ...
});

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

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

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

Спасибо за любую помощь!

Ответ 1

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

Допустим, у вас есть 200 запросов в секунду, обслуживаемых вашим сервисом. Если 1% из них попадут в исходный код в вашем коде, вы получите 20 остановок процессов в секунду, примерно один раз в 50 мс. Если у вас есть 4 ядра с 1 процессом на ядро, вы потеряете их через 200 мс. Поэтому, если процессу требуется более 200 мс для запуска и подготовки к обработке запросов (минимальная стоимость составляет около 50 мс для процесса узла, который не загружает какие-либо модули), у нас теперь есть полный отказ в обслуживании. Не говоря уже о том, что пользователи, сталкивающиеся с ошибкой, обычно делают такие вещи, как, например, многократное обновление страницы, что усугубляет проблему.

Домены не решают проблему, потому что они не могут гарантировать, что ресурсы не просочились.

Читайте больше в выпусках # 5114 и # 5149.

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

Однако обещания перехватывают все исключения и затем распространяют их способом, очень похожим на то, как синхронные исключения распространяются вверх по стеку. Кроме того, они часто предоставляют способ, finally, который предназначен, чтобы быть эквивалентом try...finally, благодаря этим двум признакам, мы можем инкапсулировать, что очистке логики путем создания "Контекст-менеджеров" ( по аналогии with в питона и using в С#), которые всегда убирают ресурсы.

Предположим, что наши ресурсы представлены как объекты с методами acquire и dispose, которые возвращают обещания. При вызове функции соединения не устанавливаются, мы только возвращаем объект ресурса. Этот объект будет обработан с using позже:

function connect(url) {
  return {acquire: cb => pg.connect(url), dispose: conn => conn.dispose()}
}

Мы хотим, чтобы API работал так:

using(connect(process.env.DATABASE_URL), async (conn) => {
  await conn.query(...);
  do other things
  return some result;
});

Мы можем легко достичь этого API:

function using(resource, fn) {
  return Promise.resolve()
    .then(() => resource.acquire())
    .then(item => 
      Promise.resolve(item).then(fn).finally(() => 
        // bail if disposing fails, for any reason (sync or async)
        Promise.resolve()
          .then(() => resource.dispose(item))
          .catch(terminate)
      )
    );
}

Ресурсы всегда будут удаляться после завершения цепочки обещаний, возвращаемой с использованием аргумента fn. Даже если в этой функции возникла ошибка (например, из JSON.parse) или ее внутренних закрытий .then (например, во втором JSON.parse), или если обещание в цепочке было отклонено (эквивалентно обратным JSON.parse, вызывающим с ошибкой). Вот почему так важно, чтобы обещания ловили ошибки и распространяли их.

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

Примечание: в конце концов terminate выбрасывает вне диапазона, поэтому обещания не могут его перехватить, например, process.nextTick(() => { throw e }); , Какая реализация имеет смысл, может зависеть от вашей настройки - основанная на nextTick работает аналогично обратным вызовам.

Как насчет использования библиотек на основе обратного вызова? Они потенциально могут быть небезопасными. Давайте посмотрим на пример, чтобы увидеть, откуда эти ошибки могут возникнуть и какие могут вызвать проблемы:

function unwrapped(arg1, arg2, done) {
  var resource = allocateResource();
  mayThrowError1();
  resource.doesntThrow(arg1, (err, res) => {
    mayThrowError2(arg2);
    done(err, res);
  });
}

mayThrowError2() находится внутри внутреннего обратного вызова и все равно завершит работу процесса, если он сгенерирует, даже если unwrapped вызывается в другом обещании .then. Подобные ошибки не улавливаются типичными promisify и продолжают вызывать сбой процесса, как обычно.

Однако mayThrowError1() будет mayThrowError1() обещанием, если оно будет mayThrowError1() внутри .then, и внутренний выделенный ресурс может просочиться.

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

function paranoidPromisify(fn) {
  return function(...args) {
    return new Promise((resolve, reject) =>   
      try {
        fn(...args, (err, res) => err != null ? reject(err) : resolve(res));
      } catch (e) {
        process.nextTick(() => { throw e; });
      }
    }
  }
}

Использование функции обещания в другом обещании. .then обратного вызова теперь происходит сбой процесса, если развернутые броски возвращаются к парадигме броска-сбоя.

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

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

function unwrapped(arg1, arg2, done) {
  var resource = allocateResource();
  resource.doSomething(arg1, function(err, res) {
    if (err) return done(err);
    resource.doSomethingElse(res, function(err, res) {
      resource.dispose();
      done(err, res);
    });
  });
}

Зачем? Потому что, когда doSomething вызов doSomething получает ошибку, код забывает утилизировать ресурс.

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

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

Ответ 2

Это почти самая важная особенность promises. Если его там не было, вы также можете использовать обратные вызовы:

var fs = require("fs");

fs.readFile("myfile.json", function(err, contents) {
    if( err ) {
        console.error("Cannot read file");
    }
    else {
        try {
            var result = JSON.parse(contents);
            console.log(result);
        }
        catch(e) {
            console.error("Invalid json");
        }
    }

});

(Прежде чем вы скажете, что JSON.parse - единственное, что бросает в js, знаете ли вы, что даже принуждение переменной к числу, например, +a, может вызывать TypeError?

Однако приведенный выше код может быть более четко выражен с помощью promises, потому что есть только один канал исключения вместо 2:

var Promise = require("bluebird");
var readFile = Promise.promisify(require("fs").readFile);

readFile("myfile.json").then(JSON.parse).then(function(result){
    console.log(result);
}).catch(SyntaxError, function(e){
    console.error("Invalid json");
}).catch(function(e){
    console.error("Cannot read file");
});

Обратите внимание, что catch - сахар для .then(null, fn). Если вы понимаете, как работает поток исключений, вы увидите, что это анти-шаблон который обычно используется .then(fnSuccess, fnFail).

Точка вовсе не, чтобы сделать .then(success, fail) над , function(fail, success) (IE это не альтернативный способ подключения ваших обратных вызовов), но сделать написанный код почти таким же, как он выглядел бы при написании синхронного кода:

try {
    var result = JSON.parse(readFileSync("myjson.json"));
    console.log(result);
}
catch(SyntaxError e) {
    console.error("Invalid json");
}
catch(Error e) {
    console.error("Cannot read file");
}

(Код синхронизации на самом деле будет более уродливым, поскольку javascript не имеет набранных уловов)

Ответ 3

Отклонение обещаний - это просто абстракция отказа. Таким образом, node -образные обратные вызовы (err, res) и исключения. Поскольку promises являются асинхронными, вы не можете использовать try-catch, чтобы на самом деле поймать что-либо, потому что ошибки могут произойти не в том же тике цикла событий.

Быстрый пример:

function test(callback){
    throw 'error';
    callback(null);
}

try {
    test(function () {});
} catch (e) {
    console.log('Caught: ' + e);
}

Здесь мы можем поймать ошибку, поскольку функция синхронна (хотя и на основе обратного вызова). Другой:

function test(callback){
    process.nextTick(function () {
        throw 'error';
        callback(null); 
    });
}

try {
    test(function () {});
} catch (e) {
    console.log('Caught: ' + e);
}

Теперь мы не можем поймать ошибку! Единственный вариант - передать его в обратном вызове:

function test(callback){
    process.nextTick(function () {
        callback('error', null); 
    });
}

test(function (err, res) {
    if (err) return console.log('Caught: ' + err);
});

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