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

Обновлять:

Чтобы помочь будущим зрителям этого сообщения, я создал эту демонстрацию ответа 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) И stepError(2) будут уволены. Если я не return $q.reject, то stepError(2) не будут уволены, но step(2) уволят, что я понимаю. Я добился всего, кроме того, что пытаюсь сделать.

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

Вот живая демонстрация, чтобы у вас было с чем поработать.

Обновлять:

Я вроде решил это. Здесь я улавливаю ошибку в конце цепочки и передаю данные 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); 
  }

person m59    schedule 21.12.2013    source источник
comment
Существует асинхронная библиотека javascript, которая может помочь, если это станет более сложным.   -  person lucuma    schedule 21.12.2013
comment
Promise.prototype.catch() примеры в MDN показать решение для тех же самых проблем.   -  person toraritte    schedule 11.09.2018


Ответы (13)


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

Допустим, у вас есть что-то вроде следующего:

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

Чтобы лучше понять, что происходит, представим, что это синхронный код с блоками _2 _ / _ 3_:

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(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 основана на библиотеке Q kriskowal (которая имеет более богатый API, но содержит все, что вы можете найти в $q). Может оказаться полезным API-документы Q на GitHub. Q реализует спецификацию Promises / A +, в которой подробно описывается, как then и обещание поведение разрешения работает точно.

РЕДАКТИРОВАТЬ:

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

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

person Alan Plum    schedule 21.12.2013
comment
Эта часть золотая: if you don't return anything, you are effectively returning a resolved promise for the value undefined. Спасибо @pluma - person Valerio; 07.04.2015
comment
Это действительно. Я редактирую его, чтобы придать ему жирный вид, которого он заслуживает - person Cyril CHAPON; 09.09.2015
comment
отклонить выход из текущей функции? например, решение не будет вызываться, если отклонение вызвано 1-м `if (bad) {reject (status); } resolve (результаты); ` - person SuperUberDuper; 05.11.2015
comment
stepOne().then(stepTwo, handleErrorOne) `stepOne (). Then (null, handleErrorOne) .then (stepTwo)` Действительно ли они эквивалентны? Я думаю, что в случае отказа в stepOne вторая строка кода выполнит stepTwo, но первая выполнит только handleErrorOne и остановится. Или я что-то упускаю? - person JeFf; 28.01.2016
comment
На самом деле не дает четкого решения заданного вопроса, тем не менее, хорошее объяснение - person Yerken; 18.03.2016
comment
Спасибо за объяснение, все так понятно :) - person Munib; 14.06.2019

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

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

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

Это позволяет вырваться из цепочки.

person Vinnyq12    schedule 19.02.2016
comment
Мне помогло, но, к вашему сведению, вы можете вернуть его тогда, чтобы вырваться из улова, например: .then(user => { if (user) return Promise.reject('The email address already exists.') }) - person Craig van Tonder; 06.01.2017
comment
@CraigvanTonder вы можете просто добавить в обещание, и он будет работать так же, как ваш код: .then(user => { if (user) throw 'The email address already exists.' }) - person Francisco Presencia; 28.01.2017
comment
Это единственно правильный ответ. В противном случае шаг 3 все равно будет выполняться, даже если на шаге 1 есть ошибка. - person wdetac; 27.02.2017
comment
Чтобы уточнить, если в stepOne () возникает ошибка, то и chainError вызывается правильно? Если это желательно. У меня есть фрагмент, который делает это, но не уверен, что я что-то неправильно понял - runkit.com/embed/9q2q3rjxdar9 - person user320550; 20.10.2017

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

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

  • Старт: позвонить step(1) безоговорочно.
  • Repeating pattern: chain a .then() with the following callbacks:
    • success: call step(n+1)
    • ошибка: выбросить значение, с которым предыдущий отложенный вызов был отклонен, или повторно выбросить ошибку.
  • Готово: связать .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 не обработает ее.

person Beetroot-Beetroot    schedule 21.12.2013
comment
Информативный ответ, поэтому его стоит оставить, но я столкнулся не с этой проблемой. Я упоминаю это решение в своем сообщении, и это не то, что я ищу. См. Демонстрацию вверху моего поста. - person m59; 21.12.2013
comment
m59, это ответ на заданный вопрос, как мне писать обещания, чтобы я мог вызывать функцию при отклонении, не вызывая все функции в цепочке ошибок? и заголовок вопроса: «Разорвать цепочку обещаний» и вызвать функцию в зависимости от шага в цепочке, на котором она нарушена (отклонена). - person Beetroot-Beetroot; 21.12.2013
comment
Правильно, как я уже сказал, это информативно, и я даже включил это решение в свой пост (с меньшими подробностями). Этот подход предназначен для исправления вещей, чтобы цепочка могла продолжаться. Хотя он может выполнить то, что я ищу, это не так естественно, как подход в принятом ответе. Другими словами, если вы хотите делать то, что выражено в названии и заданном вопросе, воспользуйтесь подходом pluma. - person m59; 21.12.2013

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

// 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);
});
person redben    schedule 19.12.2015

Если я правильно понимаю, вы хотите, чтобы показывалась только ошибка для ошибочного шага, верно?

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

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

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

person Zajn    schedule 21.12.2013
comment
Какого черта ... это именно то, что я сделал! Смотрите в моем посте, что я пробовал это, но цепочка снова включалась и запускалась step(2). Сейчас просто попробовал еще раз, этого не происходит. Я весьма озадачен. - person m59; 21.12.2013
comment
Я действительно видел, что вы упомянули об этом. Хотя это странно. Эта функция, содержащая return step(2);, должна вызываться только при успешном разрешении step(1). - person Zajn; 21.12.2013
comment
Вычеркните это - это определенно происходит. Как я уже сказал в своем посте, если вы не используете return $q.reject(), цепочка продолжит работу. В данном случае return response все испортил. См. Это: jsbin.com/EpaZIsIp/6/edit - person m59; 21.12.2013
comment
Хм, хорошо. Кажется, он работает в jsbin, который вы опубликовали, когда я его изменил, но я, должно быть, что-то пропустил. - person Zajn; 21.12.2013
comment
Да, я определенно вижу, что сейчас это не работает. Вернемся к чертежной доске для меня! - person Zajn; 21.12.2013
comment
Вздох. Неа. Я удалил свой ответ и добавил его как обновление к своему сообщению. Я решил это не так, как я хотел. - person m59; 21.12.2013

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

person Esailija    schedule 21.12.2013
comment
Но если я позвоню deferred.reject(n), тогда я получаю предупреждение, что обещание отклонено с объектом, отличным от ошибки. - person 9me; 24.04.2017

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

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);
});
person Leonid    schedule 02.05.2019

Если вы хотите решить эту проблему с помощью 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 []
    }

})()
person luispa    schedule 01.08.2019

Если в какой-то момент вы вернете Promise.reject('something'), вы попадете в блок catch для обещания.

promiseOne
  .then((result) => {
    if (!result) {
      return Promise.reject('No result');
    }
    return;
  })
  .catch((err) => {
    console.log(err);
  });

Если первое обещание не вернет никакого результата, вы получите только «Нет результата» в консоли.

person Dimitar Gospodinov    schedule 16.01.2020

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

        // 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(); });
});

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

person Ignitor    schedule 12.11.2015

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

(В принятом ответе упоминается then(null, onErrorHandler), что в основном совпадает с catch(onErrorHandler).)

Использование и связывание метода catch

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

Попадания при выкидывании ошибок

// 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's onFulfilled"); /* next promise's onFulfilled */
    console.log(value); /* calling next */
}, function (reason) {
    console.log("next promise's onRejected");
    console.log(reason);
});
person toraritte    schedule 11.09.2018

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

Я бился головой об этот шаблон уже больше года, и использовать await's - рай.

person Pete Alvin    schedule 05.10.2018
comment
При использовании чистого IE async / await не поддерживается. - person ndee; 29.11.2019

Используйте модуль 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);

Суть

SequentialPromise

person Cody    schedule 12.10.2019