Подтвердить что ты не робот

Прикованный promises не проходящий отказ

У меня проблема с пониманием того, почему отклонения не передаются через цепочку обещаний, и я надеюсь, что кто-то сможет помочь мне понять, почему. Для меня добавление функциональности в цепочку promises подразумевает намерение, что я в зависимости от оригинального обещания выполнить. Это трудно объяснить, поэтому позвольте мне сначала показать пример кода моей проблемы. (Примечание: в этом примере используется Node и отложенный модуль 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 и результата, передаваемого по цепочке. Если обещание в обещании1 не получает значения выигрышей, но вместо этого получает значение err в обработчике ошибок, как это возможно, чтобы следующее обещание в цепочке вызывало функцию успеха? Он не может передать значимую ценность следующему обещанию, потому что он не получил само значение.

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

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

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

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

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

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

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

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

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

4b9b3361

Ответ 1

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

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

Состояние документов:

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

Метод .then обычно рассматривается в терминах Promises/A спецификация (или даже более строгая Promsises/A + one). Это означает, что оболочка обратных вызовов возвращает promises, которая будет ассимилироваться, чтобы стать разрешением 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());

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

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

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

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

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

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);

Ответ 2

@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. Таким образом, семантически, синхронизация async 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 отложенной.

Краткое описание:

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

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

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

Ответ 3

Использование может обернуть ошибки на каждом уровне обещания. Я приковал ошибки в 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