Разделите цепочку обещаний и вызовите функцию, основанную на шаге в цепочке, где он сломан (отклонен)

Обновление:

Чтобы помочь будущим зрителям этого сообщения, я создал эту демонстрацию ответа pluma.

Вопрос:

Моя цель кажется довольно простой.

  step(1)
  .then(function() {
    return step(2);
  }, function() {
    stepError(1);
    return $q.reject();
  })
  .then(function() {

  }, function() {
    stepError(2);
  });

  function step(n) {
    var deferred = $q.defer();
    //fail on step 1
    (n === 1) ? deferred.reject() : deferred.resolve();
    return deferred.promise;
  }
  function stepError(n) {
    console.log(n); 
  }

Проблема здесь в том, что если я не сработаю на шаге 1, будут запущены как stepError(1) AND stepError(2). Если я не return $q.reject, то stepError(2) не будет запущен, но step(2) будет, что я понимаю. Я выполнил все, кроме того, что я пытаюсь сделать.

Как написать promises, чтобы я мог вызывать функцию при отказе, не вызывая все функции в цепочке ошибок? Или есть другой способ сделать это?

Вот живая демонстрация, с которой вы что-то работаете.

Обновление:

I вид решил. Здесь я поймаю ошибку в конце цепочки и передаю данные на reject(data), чтобы я знал, какую проблему обрабатывать в функции ошибки. Это действительно не соответствует моим требованиям, потому что я не хочу зависеть от данных. Это было бы хромой, но в моем случае было бы проще передать обратный вызов ошибки функции, а не зависеть от возвращаемых данных, чтобы определить, что делать.

Живая демонстрация здесь (нажмите).

step(1)
  .then(function() {
    return step(2);
  })
  .then(function() {
    return step(3);
  })
  .then(false, 
    function(x) {
      stepError(x);
    }
  );
  function step(n) {
    console.log('Step '+n);
    var deferred = $q.defer();
    (n === 1) ? deferred.reject(n) : deferred.resolve(n);
    return deferred.promise;
  }
  function stepError(n) {
    console.log('Error '+n); 
  }

Ответ 1

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

Скажем, у вас есть что-то вроде следующего:

stepOne()
.then(stepTwo, handleErrorOne)
.then(stepThree, handleErrorTwo)
.then(null, handleErrorThree);

Чтобы лучше понять, что происходит, предположим, что это синхронный код с блоками try/catch:

try {
    try {
        try {
            var a = stepOne();
        } catch(e1) {
            a = handleErrorOne(e1);
        }
        var b = stepTwo(a);
    } catch(e2) {
        b = handleErrorTwo(e2);
    }
    var c = stepThree(b);
} catch(e3) {
    c = handleErrorThree(e3);
}

Обработчик onRejected (второй аргумент then) по существу является механизмом исправления ошибок (например, блоком catch). Если ошибка возникает в handleErrorOne, она будет улавливаться следующим блоком catch (catch(e2)) и т.д.

Это явно не то, что вы намеревались.

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

stepOne()
.then(function(a) {
    return stepTwo(a).then(null, handleErrorTwo);
}, handleErrorOne)
.then(function(b) {
    return stepThree(b).then(null, handleErrorThree);
});

Примечание. Мы можем оставить handleErrorOne там, где он есть, потому что он будет вызываться только в том случае, если stepOne отклоняет (это первая функция в цепочке, поэтому мы знаем, что если цепь отклоняется в этот момент, может быть только потому, что обе функции обещают).

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

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

Это означает, что следующие две строки эквивалентны:

stepOne().then(stepTwo, handleErrorOne)
stepOne().then(null, handleErrorOne).then(stepTwo)

Но следующая строка не эквивалентна приведенной выше:

stepOne().then(stepTwo).then(null, handleErrorOne)

Angular библиотека обещаний $q основана на библиотеке kriskowal Q (которая имеет более богатый API, но содержит все, что вы можете найти в $q). Q API docs на GitHub может оказаться полезным. Q реализует Promises/A + spec, в котором подробно описывается, как then и поведение разрешения обещания работает точно.

EDIT:

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

Это означает, что если вы ничего не возвращаете, вы фактически возвращаете разрешенное обещание для значения undefined.

Ответ 2

Бит опоздал на вечеринку, но это простое решение сработало для меня:

function chainError(err) {
  return Promise.reject(err)
};

stepOne()
.then(stepTwo, chainError)
.then(stepThreee, chainError);

Это позволяет вам выйти из цепочки.

Ответ 3

Вам нужна повторяющаяся цепочка .then() со специальным футляром для запуска и завершение специального случая.

Умение состоит в том, чтобы получить номер шага случая отказа, чтобы прорваться к конечному обработчику ошибок.

  • Начать: вызывать step(1) безоговорочно.
  • Повторяющийся шаблон: цепочка a .then() со следующими обратными вызовами:
    • успех: шаг вызова (n + 1)
    • failure: выкинуть значение, с которым предыдущий отложенный был отклонен или сбросить ошибку.
  • Finish: цепочка a .then() без обработчика успеха и конечного обработчика ошибок.

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

function nextStep(n) {
    return step(n + 1);
}

function step(n) {
    console.log('step ' + n);
    var deferred = $q.defer();
    (n === 3) ? deferred.reject(n) : deferred.resolve(n);
    return deferred.promise;
}

function stepError(n) {
    throw(n);
}

function finalError(n) {
    console.log('finalError ' + n);
}
step(1)
    .then(nextStep, stepError)
    .then(nextStep, stepError)
    .then(nextStep, stepError)
    .then(nextStep, stepError)
    .then(nextStep, stepError)
    .then(null, finalError);});

см. демо

Обратите внимание, что в step() отложенное отклоняется или разрешается с помощью n, тем самым делая это значение доступным для обратных вызовов в следующем .then() в цепочке. Когда вызывается stepError, ошибка повторяется повторно, пока она не обрабатывается finalError.

Ответ 4

При отказе следует передать ошибку отклонения, а затем обработать обработчики ошибок шага в функции, которая проверяет, должна ли обработка отклонения быть обработана или "возвращена" до конца цепочки:

// function mocking steps
function step(i) {
    i++;
    console.log('step', i);
    return q.resolve(i);
}

// function mocking a failing step
function failingStep(i) {
    i++;
    console.log('step '+ i + ' (will fail)');
    var e = new Error('Failed on step ' + i);
    e.step = i;
    return q.reject(e);
}

// error handler
function handleError(e){
    if (error.breakChain) {
        // handleError has already been called on this error
        // (see code bellow)
        log('errorHandler: skip handling');
        return q.reject(error);
    }
    // firs time this error is past to the handler
    console.error('errorHandler: caught error ' + error.message);
    // process the error 
    // ...
    //
    error.breakChain = true;
    return q.reject(error);
}

// run the steps, will fail on step 4
// and not run step 5 and 6
// note that handleError of step 5 will be called
// but since we use that error.breakChain boolean
// no processing will happen and the error will
// continue through the rejection path until done(,)

  step(0) // 1
  .catch(handleError)
  .then(step) // 2
  .catch(handleError)
  .then(step) // 3
  .catch(handleError)
  .then(failingStep)  // 4 fail
  .catch(handleError)
  .then(step) // 5
  .catch(handleError)
  .then(step) // 6
  .catch(handleError)
  .done(function(){
      log('success arguments', arguments);
  }, function (error) {
      log('Done, chain broke at step ' + error.step);
  });

Что вы увидите на консоли:

step 1
step 2
step 3
step 4 (will fail)
errorHandler: caught error 'Failed on step 4'
errorHandler: skip handling
errorHandler: skip handling
Done, chain broke at step 4

Вот какой рабочий код https://jsfiddle.net/8hzg5s7m/3/

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

/*
 * simple wrapper to check if rejection
 * has already been handled
 * @param function real error handler
 */
function createHandler(realHandler) {
    return function(error) {
        if (error.breakChain) {
            return q.reject(error);
        }
        realHandler(error);
        error.breakChain = true;
        return q.reject(error);    
    }
}

тогда ваша цепочка

step1()
.catch(createHandler(handleError1Fn))
.then(step2)
.catch(createHandler(handleError2Fn))
.then(step3)
.catch(createHandler(handleError3Fn))
.done(function(){
    log('success');
}, function (error) {
    log('Done, chain broke at step ' + error.step);
});

Ответ 5

Если я правильно понял, вам нужна только ошибка для неудачного шага, правильно?

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

step(1).then(function (response) {
    step(2);
}, function (response) {
    stepError(1);
    return response;
}).then( ... )

Возвращая $q.reject() в случае сбоя первого шага, вы отклоняете это обещание, которое вызывает вызов errorCallback во втором then(...).

Ответ 6

var s = 1;
start()
.then(function(){
    return step(s++);
})
.then(function() {
    return step(s++);
})
.then(function() {
    return step(s++);
})
.then(0, function(e){
   console.log(s-1); 
});

http://jsbin.com/EpaZIsIp/20/edit

Или автоматизировано для любого количества шагов:

var promise = start();
var s = 1;
var l = 3;
while(l--) {
    promise = promise.then(function() {
        return step(s++);
    });
}
promise.then(0, function(e){
   console.log(s-1); 
});

http://jsbin.com/EpaZIsIp/21/edit

Ответ 7

Если вы хотите решить эту проблему, используя async/await:

(async function(){    
    try {        
        const response1, response2, response3
        response1 = await promise1()

        if(response1){
            response2 = await promise2()
        }
        if(response2){
            response3 = await promise3()
        }
        return [response1, response2, response3]
    } catch (error) {
        return []
    }

})()

Ответ 8

Прикрепите обработчики ошибок как отдельные элементы цепи непосредственно к выполнению шагов:

        // Handle errors for step(1)
step(1).then(null, function() { stepError(1); return $q.reject(); })
.then(function() {
                 // Attach error handler for step(2),
                 // but only if step(2) is actually executed
  return step(2).then(null, function() { stepError(2); return $q.reject(); });
})
.then(function() {
                 // Attach error handler for step(3),
                 // but only if step(3) is actually executed
  return step(3).then(null, function() { stepError(3); return $q.reject(); });
});

или используя catch():

       // Handle errors for step(1)
step(1).catch(function() { stepError(1); return $q.reject(); })
.then(function() {
                 // Attach error handler for step(2),
                 // but only if step(2) is actually executed
  return step(2).catch(function() { stepError(2); return $q.reject(); });
})
.then(function() {
                 // Attach error handler for step(3),
                 // but only if step(3) is actually executed
  return step(3).catch(function() { stepError(3); return $q.reject(); });
});

Примечание: это в основном тот же шаблон, что и плюма предлагает в своем ответе, но с использованием имен OP.

Ответ 9

Найденные Promise.prototype.catch() примеры на MDN ниже очень полезны.

(Принятый ответ упоминает then(null, onErrorHandler) который в основном такой же, как catch(onErrorHandler).)

Использование и привязка метода улова

var p1 = new Promise(function(resolve, reject) {
  resolve('Success');
});

p1.then(function(value) {
  console.log(value); // "Success!"
  throw 'oh, no!';
}).catch(function(e) {
  console.log(e); // "oh, no!"
}).then(function(){
  console.log('after a catch the chain is restored');
}, function () {
  console.log('Not fired due to the catch');
});

// The following behaves the same as above
p1.then(function(value) {
  console.log(value); // "Success!"
  return Promise.reject('oh, no!');
}).catch(function(e) {
  console.log(e); // "oh, no!"
}).then(function(){
  console.log('after a catch the chain is restored');
}, function () {
  console.log('Not fired due to the catch');
});

Gotchas при метании ошибок

// Throwing an error will call the catch method most of the time
var p1 = new Promise(function(resolve, reject) {
  throw 'Uh-oh!';
});

p1.catch(function(e) {
  console.log(e); // "Uh-oh!"
});

// Errors thrown inside asynchronous functions will act like uncaught errors
var p2 = new Promise(function(resolve, reject) {
  setTimeout(function() {
    throw 'Uncaught Exception!';
  }, 1000);
});

p2.catch(function(e) {
  console.log(e); // This is never called
});

// Errors thrown after resolve is called will be silenced
var p3 = new Promise(function(resolve, reject) {
  resolve();
  throw 'Silenced Exception!';
});

p3.catch(function(e) {
   console.log(e); // This is never called
});

Если он разрешен

//Create a promise which would not call onReject
var p1 = Promise.resolve("calling next");

var p2 = p1.catch(function (reason) {
    //This is never called
    console.log("catch p1!");
    console.log(reason);
});

p2.then(function (value) {
    console.log("next promise onFulfilled"); /* next promise onFulfilled */
    console.log(value); /* calling next */
}, function (reason) {
    console.log("next promise onRejected");
    console.log(reason);
});

Ответ 10

Лучшее решение - реорганизовать вашу цепочку обещаний на использование ожиданий ES6. Затем вы можете просто вернуться из функции, чтобы пропустить остальную часть поведения.

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

Ответ 11

Попробуйте использовать это как libs:

https://www.npmjs.com/package/promise-chain-break

    db.getData()
.then(pb((data) => {
    if (!data.someCheck()) {
        tellSomeone();

        // All other '.then' calls will be skiped
        return pb.BREAK;
    }
}))
.then(pb(() => {
}))
.then(pb(() => {
}))
.catch((error) => {
    console.error(error);
});

Ответ 12

Используйте модуль SequentialPromise

Намерение

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

Участники

  • Контекст: объект, метод-член которого выполняет операцию.
  • SequentialPromise: определяет метод execute для цепочки & отслеживать каждую операцию. SequentialPromise возвращает цепочку обещаний для всех выполненных операций.
  • Invoker: создает экземпляр SequentialPromise, предоставляя ему контекст & действие и вызывает его метод execute при передаче порядкового списка параметров для каждой операции.

Последствия

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

Реализация
clear();

var http = {
    get(url) {
        var delay = Math.floor( Math.random() * 10 ), even = !(delay % 2);
        var xhr = new Promise(exe);

        console.log('REQUEST', url, delay);
        xhr.then( (data) => console.log('SUCCESS: ', data) ).catch( (data) => console.log('FAILURE: ', data) );

        function exe(resolve, reject) {
            var action = { 'true': reject, 'false': resolve }[ even ];
            setTimeout( () => action({ url, delay }), (1000 * delay) );
        }

        return xhr;
    }
};

var SequentialPromise = new (function SequentialPromise() {
    var PRIVATE = this;

    return class SequentialPromise {

        constructor(context, action) {
            this.index = 0;
            this.requests = [ ];
            this.context = context;
            this.action = action;

            return this;
        }

        log() {}

        execute(url, ...more) {
            var { context, action, requests } = this;
            var chain = context[action](url);

            requests.push(chain);
            chain.then( (data) => this.index += 1 );

            if (more.length) return chain.then( () => this.execute(...more) );
            return chain;
        }

    };
})();

var sequence = new SequentialPromise(http, 'get');
var urls = [
    'url/name/space/0',
    'url/name/space/1',
    'url/name/space/2',
    'url/name/space/3',
    'url/name/space/4',
    'url/name/space/5',
    'url/name/space/6',
    'url/name/space/7',
    'url/name/space/8',
    'url/name/space/9'
];
var chain = sequence.execute(...urls);
var promises = sequence.requests;

chain.catch( () => console.warn('EXECUTION STOPPED at ${sequence.index} for ${urls[sequence.index]}') );

// console.log('>', chain, promises);

Gist

SequentialPromise