Розрив ланцюга обіцянок і виклик функції на основі кроку в ланцюжку, де він розірваний (відхилено)


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

1
Існує асинхрована javascript lib, яка може допомогти, якщо це стане складніше
lucuma

Promise.prototype.catch()приклади на MDN показують вирішення точно таких же проблем.
toraritte

Відповіді:


199

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

Скажімо, у вас є щось на кшталт наступного:

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(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
Це справді. Я
редагую

чи відхиляє вихід поточної функції? наприклад, розв’язання не буде викликано, якщо відхилення називається 1st `if (bad) {reject (status); } рішення (результати); `
SuperUberDuper

stepOne().then(stepTwo, handleErrorOne) `stepOne (). тоді (null, handleErrorOne) .then (stepTwo)` Чи ці цілком еквівалентні? Я думаю, що у випадку відхилення в stepOneдругому рядку коду буде виконуватися, stepTwoале перший виконає handleErrorOneі зупинить. Або я щось пропускаю?
JeFf

5
Насправді не передбачено чіткого вирішення поставленого питання, все-таки хорошого пояснення
Yerken

57

Трохи запізнився на вечірку, але це просте рішення працювало для мене:

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

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

Це дозволяє вирватися з ланцюга.


1
Допомогли мені, але FYI, ви можете повернути його в той час, щоб вибухнути в улові на кшталт:.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)беззастережно.
  • Повторювана схема: ланцюжок a .then()з наступними зворотними викликами:
    • успіх: крок виклику (n + 1)
    • Невдача: кинути значення, з яким попередній відхилений був відхилений, або повторно скинути помилку.
  • Завершення: ланцюг 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.


Інформативна відповідь, тому її варто тримати, але це не проблема, з якою я стикаюся. Я згадую це рішення у своєму пості, і це не те, що я шукаю. Дивіться демонстрацію вгорі мого повідомлення.
m59

1
m59, це відповідь на поставлене запитання, "як я пишу обіцянки, щоб я міг викликати функцію при відхиленні, не викликаючи всіх функцій ланцюга помилок?" і назва питання, «Перерва обіцянку ланцюга і викликати функцію на основі кроку в ланцюзі , де порушується (відхилено)»
Буряк буряк-

Правильно, як я вже сказав, це інформативно, і я навіть включив це рішення у свій пост (з меншою деталізацією). Цей підхід призначений для фіксації речей, щоб ланцюг міг продовжуватись. Хоча це може зробити те, що я шукаю, це не так природно, як підхід у прийнятій відповіді. Іншими словами, якщо ви хочете виконати те, що виражається в заголовку та заданому питанні, скористайтеся підходом 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);
});

2

Якщо я правильно розумію, ви хочете показати лише помилку за невдалий крок, правда?

Це повинно бути таким же простим, як і змінити випадок відмови першої обіцянки:

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

Повернувшись $q.reject()у випадку відмови першого кроку, ви відкидаєте цю обіцянку, через що у 2-му виклику буде виклик помилки then(...).


Що на світі ... саме так я і зробив! Подивіться у своєму дописі, що я це намагався, але ланцюг би відскочив і забіг 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


Але якщо я зателефоную, deferred.reject(n)я отримую попередження, що обіцянку відхилено об’єктом
nonError


2

Якщо ви хочете вирішити цю проблему за допомогою async / wait:

(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 []
    }

})()

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

Примітка. Це в основному та ж закономірність, яку пропонує слива у своїй відповіді, але використовуючи найменування ОП.


1

Знайдені нижче 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');
});

Потрапляє при помилках

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

1

Найкраще рішення - рефакторинг ланцюга ваших обіцянок, щоб використовувати ES6 очікування. Тоді ви можете просто повернутися з функції, щоб пропустити решту поведінки.

Я б'ю головою об цю картину вже більше року і використовую функцію очікування - це небо.


При використанні чистого IE async / await не підтримується.
ndee

0

Використовуйте модуль SequencesPromise

Намір

Надайте модуль, відповідальність за який полягає у виконанні запитів послідовно, одночасно відслідковуючи поточний індекс кожної операції. Визначте операцію в командному шаблоні для гнучкості.

Учасники

  • Контекст : Об'єкт, метод якого член виконує операцію.
  • SequentialPromise : визначає executeметод ланцюга та відстеження кожної операції. SequentialPromise повертає Promise-Chain з усіх виконаних операцій.
  • Invoker : Створює екземпляр SequtialPromise , надаючи йому контекст та дію, і викликає його executeметод, переходячи до порядкового списку параметрів для кожної операції.

Наслідки

Використовуйте SequentialPromise, коли потрібна порядкова поведінка дозволу Обіцяння. 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);

Суть

Послідовна Проміз


0

Якщо в будь-який момент ви повернетесь, Promise.reject('something')вас кинуть у блок улову до обіцянки.

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

Якщо перша обіцянка не принесе жодного результату, ви отримаєте лише "Без результату" в консолі.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.