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

135

Обновить:

Чтобы помочь будущим зрителям этого поста, я создал демо-версию ответа 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); 
  }
M59
источник
1
Существует асинхронная библиотека javascript, которая может помочь, если это станет еще сложнее
lucuma
Promise.prototype.catch()примеры на MDN показывают решение точно таких же проблем.
Тораритт

Ответы:

199

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

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

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

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

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

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

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

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

Алан слива
источник
138
Эта часть золотая: if you don't return anything, you are effectively returning a resolved promise for the value undefined.Спасибо @pluma
Валерио
7
Это действительно. Я редактирую это, чтобы дать этому смелое, которого это заслуживает
Кирилл Чэпон
Отклонить выход из текущей функции? например, resolv не будет вызываться, если reject вызывается 1st `if (bad) {reject (status); }
resolution
stepOne().then(stepTwo, handleErrorOne) `stepOne (). then (null, handleErrorOne) .then (stepTwo)` Являются ли они полностью эквивалентными? Я думаю, что в случае отклонения во stepOneвторой строке кода будет выполняться, stepTwoно первая будет только выполнить handleErrorOneи остановить. Или я что-то упустил?
JeFf
5
На самом деле не дает четкого решения поставленного вопроса, тем не менее, хорошее объяснение
Еркен
57

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

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

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

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

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

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

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

  • Начало: звоните step(1)безоговорочно.
  • Повторяющийся шаблон: цепочка А .then()со следующими обратными вызовами:
    • успех: вызов шага (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.

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

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

// 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);
});
redben
источник
2

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

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

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

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

Zajn
источник
Что в мире ... это именно то, что я сделал! Посмотрите в моем посте, что я попробовал это, но цепь сработает step(2). Сейчас я просто попробовал еще раз, этого не происходит. Я весьма озадачен.
m59
1
Я видел, что вы упомянули это. Это странно, хотя. Эта функция return step(2);должна вызываться только при step(1)успешном разрешении.
Зайн
Поцарапайте это - это определенно происходит. Как я сказал в своем посте, если вы не используете return $q.reject(), цепь будет продолжать работать . В этом случае все return responseиспортилось. Смотрите это: jsbin.com/EpaZIsIp/6/edit
m59
Хм, хорошо. Кажется, он работает в jsbin, который вы разместили, когда я изменил это, но я, должно быть, что-то пропустил.
Зайн
Да, я определенно вижу, что сейчас не работает. Вернуться к чертежной доске для меня!
Зайн
2
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

Esailija
источник
Но если я позвоню, deferred.reject(n)то получу предупреждение, что обещание отклонено с помощью объекта
nonError
2

Попробуйте использовать это как 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);
});
Леонид
источник
2

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

})()
luispa
источник
1

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

        // 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.

Ignitor
источник
1

Найдены 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);
});
toraritte
источник
1

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

Я бью себя по этому шаблону больше года, и я жду, что это - рай.

Пит Элвин
источник
При использовании чистого IE async / await не поддерживается.
до
0

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

умысел

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

участники

  • Контекст : объект, метод-член которого выполняет операцию.
  • SequentialPromise : определяет executeметод для цепочки и отслеживания каждой операции. SequentialPromise возвращает Promise-Chain из всех выполненных операций.
  • 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

Cody
источник
0

Если в какой-то момент вы вернетесь, Promise.reject('something')вы будете брошены в блоке улова к обещанию.

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

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

Димитар Господинов
источник