Цепные обещания не переходят на отказ

У меня проблемы с пониманием того, почему отказы не передаются через цепочку обещаний, и я надеюсь, что кто-то сможет помочь мне понять, почему. Для меня присоединение функциональности к цепочке обещаний подразумевает намерение, которое я полагаюсь на первоначальное обещание, которое должно быть выполнено. Это сложно объяснить, поэтому позвольте мне сначала показать пример кода моей проблемы. (Примечание: в этом примере используются Node и модуль отсроченного узла. Я протестировал это с Dojo 1.8.3 и получил те же результаты)

var d = require("deferred");

var d1 = d();

var promise1 = d1.promise.then(
    function(wins) { console.log('promise1 resolved'); return wins;},
    function(err) { console.log('promise1 rejected'); return err;});
var promise2 = promise1.then(
    function(wins) { console.log('promise2 resolved'); return wins;},
    function(err) { console.log('promise2 rejected'); return err;});
var promise3 = promise2.then(
    function(wins) { console.log('promise3 resolved'); return wins;},
    function(err) { console.log('promise3 rejected'); return err;});
d1.reject(new Error());

Результатами выполнения этой операции являются следующие выходные данные:

promise1 rejected
promise2 resolved
promise3 resolved

Ладно, для меня этот результат не имеет смысла. Присоединяясь к этой цепочке обещаний, каждый из них подразумевает намерение, что оно будет зависеть от успешного разрешения d1 и результата, передаваемого по цепочке. Если обещание в promise1 не получает значение выигрыша, а вместо этого получает значение err в своем обработчике ошибок, как возможно, чтобы следующее обещание в цепочке вызвало функцию успеха? Невозможно передать значимое значение следующему обещанию, потому что оно само не получило значения.

Я могу описать по-другому то, о чем я думаю: есть три человека, Джон, Джинджер и Боб. Джон владеет магазином виджетов. Джинджер заходит в свой магазин и запрашивает сумку виджетов разных цветов. У него их нет в наличии, поэтому он отправляет запрос своему дистрибьютору, чтобы они отправили его ему. В то же время, он дает Джинджер проверку дождя, заявляя, что он должен ей пакет виджетов. Боб узнает, что Джинджер получает виджеты и просит, чтобы он получил синий виджет, когда она закончит с ними. Она соглашается и дает ему записку о том, что она будет. Теперь дистрибьютор Джона не может найти какие-либо виджеты в своем ассортименте, а производитель их больше не производит, поэтому они информируют Джона, который, в свою очередь, сообщает Джинджер, что она не может получить виджеты. Как Боб может получить синий виджет от Джинджер, когда сам не получил?

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

var promise = db.query({parent_id: value});
promise.then(function(query_result) {
    var first_value = {
        parent_id: query_result[0].parent_id
    }
    var promise = db.put(first_value);
    promise.then(function(first_value_result) {
        var second_value = {
            reference_to_first_value_id: first_value_result.id
        }
        var promise = db.put(second_value);
        promise.then(function(second_value_result) {
            values_successfully_entered();
        }, function(err) { return err });
    }, function(err) { return err });
}, function(err) { return err });

Теперь, в этой ситуации, если db.query потерпел неудачу, он вызвал бы функцию err для first. Но тогда это вызвало бы функцию успеха следующего обещания. Хотя это обещание ожидает результатов первого значения, оно вместо этого получит сообщение об ошибке из своей функции обработчика ошибок.

Итак, мой вопрос: зачем мне использовать функцию обработки ошибок, если мне нужно проверить ошибки в моей функции успеха?

Извините за длину этого. Я просто не знал, как объяснить это по-другому.

ОБНОВЛЕНИЕ и исправление

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

Спасибо всем, кто ответил. Я хотел бы сначала извиниться перед всеми за то, что так плохо написал мой вопрос, особенно мой псевдокод. Я был слишком агрессивен, пытаясь сократить его.

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

4 ответа

Решение

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

Нет. То, что вы описываете, это не цепочка, а просто прикрепление всех обратных вызовов к d1, Тем не менее, если вы хотите связать что-то с then, результат для promise2 зависит от разрешения promise1 и как then обратные вызовы справились с этим.

Документы утверждают:

Возвращает новое обещание для результата обратных вызовов.

.then метод обычно рассматривается в терминах спецификации Promises / A (или даже более строгих Promsises / A + one). Это означает, что ответные вызовы оболочки обратного вызова будут ассимилированы, чтобы стать разрешением promise2 и если нет обработчика успеха / ошибки, соответствующий результат будет передан непосредственно в promise2 - так что вы можете просто опустить обработчик для распространения ошибки.

Тем не менее, если ошибка обрабатывается, результирующий promise2 рассматривается как фиксированный и будет выполняться с этим значением. Если вы не хотите этого, вам придется повторно throw ошибка, как в предложении try-catch. В качестве альтернативы вы можете вернуть (чтобы быть) отклоненное обещание от обработчика. Не уверен, что такое отказ от Dojo, но:

var d1 = d();

var promise1 = d1.promise.then(
    function(wins) { console.log('promise1 resolved'); return wins;},
    function(err) { console.log('promise1 rejected'); throw err;});
var promise2 = promise1.then(
    function(wins) { console.log('promise2 resolved'); return wins;},
    function(err) { console.log('promise2 rejected'); throw err;});
var promise3 = promise2.then(
    function(wins) { console.log('promise3 resolved'); return wins;},
    function(err) { console.log('promise3 rejected'); throw err;});
d1.reject(new Error());

Как Боб может получить синий виджет от Джинджер, когда сам не получил?

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

Чтобы перевести ваши обратные вызовы ошибок в метафер, return err из обработчика все равно, что сказать: "если не осталось виджетов, просто отметьте, что их не осталось - это так же хорошо, как нужный виджет".

В ситуации с базой данных, если db.query потерпел неудачу, он вызвал бы функцию err первого, затем

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

var promise = db.query({parent_id: value});
promise.then(function(query_result) {
    var first_value = {
        parent_id: query_result[0].parent_id
    }
    var promise = db.put(first_value);
    return promise.then(function(first_value_result) {
        var second_value = {
            reference_to_first_value_id: first_value_result.id
        }
        var promise = db.put(second_value);
        return promise.then(function(second_value_result) {
            return values_successfully_entered();
        });
    });
});

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

db.query({parent_id: value}).then(function(query_result) {
    return db.put({
        parent_id: query_result[0].parent_id
    });
}).then(function(first_value_result) {
    return db.put({
        reference_to_first_value_id: first_value_result.id
    });
}.then(values_successfully_entered);

@Jordan, во-первых, как отметили комментаторы, при использовании отложенной библиотеки ваш первый пример определенно дает ожидаемый результат:

promise1 rejected
promise2 rejected
promise3 rejected

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

promise.then(function(first_value) {
    console.log('promise1 resolved');
    var promise = db.put(first_value);
    promise.then(function (second_value) {
         console.log('promise2 resolved');
         var promise = db.put(second_value);
         promise.then(
             function (wins) { console.log('promise3 resolved'); },
             function (err) { console.log('promise3 rejected'); return err; });
    }, function (err) { console.log('promise2 rejected'); return err;});
}, function (err) { console.log('promise1 rejected'); return err});

и что в случае отклонения первого обещания просто выдаст:

promise1 rejected

Однако (переходя к самой интересной части), хотя отложенная библиотека определенно возвращает 3 x rejected большинство других библиотек обещаний вернутся 1 x rejected, 2 x resolved (это приводит к предположению, что вы получили эти результаты, используя вместо этого какую-то другую библиотеку обещаний).

Что еще более запутанно, эти другие библиотеки более корректны в своем поведении. Позволь мне объяснить.

В мире синхронизации аналог "обещания отклонения" throw, Так семантически, асинхронно deferred.reject(new Error()) синхронно равно throw new Error(), В вашем примере вы не генерируете ошибки в своих обратных вызовах синхронизации, вы просто возвращаете их, поэтому вы переключаетесь на поток успеха, где ошибка является значением успеха. Чтобы убедиться, что отклонение передается дальше, вам необходимо повторно выдать свои ошибки:

function (err) { console.log('promise1 rejected'); throw err; });

Итак, теперь вопрос, почему отложенная библиотека приняла возвращенную ошибку как отклонение?

Причиной этого является то, что отказ в отложенном работает немного по-другому. В отложенной lib правило: обещание отклоняется, когда оно разрешается с ошибкой, так что даже если вы делаете deferred.resolve(new Error()) это будет действовать как deferred.reject(new Error()) и если вы попытаетесь сделать deferred.reject(notAnError) он выдаст исключение, сказав, что обещание может быть отклонено только в случае ошибки. Это объясняет, почему ошибка возвращается из then обратный вызов отклоняет обещание.

Есть некоторая веская аргументация в пользу отложенной логики, но все же она не соответствует throw работает в JavaScript, и из-за этого это поведение планируется изменить с версией v0.7 от отложенного.

Краткое содержание:

Чтобы избежать путаницы и неожиданных результатов, просто следуйте правилам хорошей практики:

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

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

простое объяснение отсюда:

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

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

В приведенном ниже примере .catch успешно обрабатывает ошибку:

      new Promise((resolve, reject) => {

  throw new Error("Whoops!");

}).catch(function(error) {

  alert("The error is handled, continue normally");

}).then(() => alert("Next successful handler runs"));

Здесьcatchблок завершается нормально. Так что следующий успешныйthenобработчик вызывается.

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

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

      new Promise((resolve, reject) => {

  throw new Error("Whoops!");

}).catch(function(error) { // (*) first catch

  if (error instanceof URIError) { //just as example
    // handle it...
  } else {
    alert("Can't handle such error");
    throw error; // throwing this jumps to the next catch
  }

}).then(function() {

  // our error is other than URIError, so: 
  // the code doesn't reach here (jump to next catch)

}).catch(error => { // (**) second catch

  alert(`The unknown error has occurred: ${error}`);
  // don't return anything => execution goes the normal way

});

В приведенном выше примере мы видим, что первая функция catch (*) перехватывает ошибку, но не может ее обработать (например, она знает только, как обрабатывать URIError), поэтому выдает ее снова. Выполнение переходит от первого улова (*) к следующему (**) вниз по цепочке.

Использование может переносить ошибки на каждом уровне Обещания. Я приковал ошибки в TraceError:

class TraceError extends Error {
  constructor(message, ...causes) {
    super(message);

    const stack = Object.getOwnPropertyDescriptor(this, 'stack');

    Object.defineProperty(this, 'stack', {
      get: () => {
        const stacktrace = stack.get.call(this);
        let causeStacktrace = '';

        for (const cause of causes) {
          if (cause.sourceStack) { // trigger lookup
            causeStacktrace += `\n${cause.sourceStack}`;
          } else if (cause instanceof Error) {
            causeStacktrace += `\n${cause.stack}`;
          } else {
            try {
              const json = JSON.stringify(cause, null, 2);
              causeStacktrace += `\n${json.split('\n').join('\n    ')}`;
            } catch (e) {
              causeStacktrace += `\n${cause}`;
              // ignore
            }
          }
        }

        causeStacktrace = causeStacktrace.split('\n').join('\n    ');

        return stacktrace + causeStacktrace;
      }
    });

    // access first error
    Object.defineProperty(this, 'cause', {value: () => causes[0], enumerable: false, writable: false});

    // untested; access cause stack with error.causes()
    Object.defineProperty(this, 'causes', {value: () => causes, enumerable: false, writable: false});
  }
}

использование

throw new TraceError('Could not set status', srcError, ...otherErrors);

Выход

функции

TraceError#cause - first error
TraceError#causes - list of chained errors
Другие вопросы по тегам