Обробка декількох уловів у ланцюжку обіцянок


125

Я все ще досить обіцяний і зараз використовую блакитну птицю, однак у мене є сценарій, коли я не зовсім впевнений, як найкраще впоратися з цим.

Так, наприклад, я маю ланцюжок обіцянок у експрес-додатку, як:

repository.Query(getAccountByIdQuery)
        .catch(function(error){
            res.status(404).send({ error: "No account found with this Id" });
        })
        .then(convertDocumentToModel)
        .then(verifyOldPassword)
        .catch(function(error) {
            res.status(406).send({ OldPassword: error });
        })
        .then(changePassword)
        .then(function(){
            res.status(200).send();
        })
        .catch(function(error){
            console.log(error);
            res.status(500).send({ error: "Unable to change password" });
        });

Тож поведінка, яку я переслідую, така:

  • Іде отримати рахунок від Id
  • Якщо в цьому місці є відхилення, вибухніть і поверніть помилку
  • Якщо помилки немає, конвертуйте документ, повернутий у модель
  • Перевірте пароль за допомогою документа бази даних
  • Якщо паролі не збігаються, вибухнуть і повернуть іншу помилку
  • Якщо помилки немає, змініть паролі
  • Тоді поверніть успіх
  • Якщо щось пішло не так, поверніть 500

Тож зараз ловчі, схоже, не зупиняють ланцюг, і це має сенс, тому мені цікаво, чи є спосіб, щоб я якось змусив ланцюг зупинитися в певний момент на основі помилок, чи є кращий спосіб структурувати це, щоб отримати певну форму розгалуженої поведінки, як це має місце if X do Y else Z.

Будь-яка допомога була б чудовою.


Чи можете ви переосмислитись чи рано повернутися?
Pieter21

Відповіді:


126

Така поведінка точно схожа на синхронний кидок:

try{
    throw new Error();
} catch(e){
    // handle
} 
// this code will run, since you recovered from the error!

Це половина сенсу .catch- мати можливість відновитися після помилок. Можливо, захочеться повторно скинути, щоб сигналізувати, що стан все ще є помилкою:

try{
    throw new Error();
} catch(e){
    // handle
    throw e; // or a wrapper over e so we know it wasn't handled
} 
// this code will not run

Однак, це не допоможе у вашому випадку, оскільки помилка буде виявлена ​​пізнішим обробником. Справжня проблема тут полягає в тому, що узагальнені обробники помилок "HANDLE ANTHTHING" є загальною помилковою практикою і вкрай спокушені іншими мовами програмування та екосистемами. З цієї причини Bluebird пропонує набрані та предикатні улови.

Додатковою перевагою є те, що ваша бізнес-логіка взагалі не повинна (і не повинна) знати про цикл запитів / відповідей. Відповідальність на запит не визначає, який статус та помилку HTTP отримує клієнт, і пізніше, коли додаток зростає, ви, можливо, захочете відокремити бізнес-логіку (як запитувати вашу БД і як обробити ваші дані) від того, що ви надсилаєте клієнту. (який код статусу http, який текст та відповідь).

Ось як я напишу ваш код.

По-перше, я б міг .Queryкинути NoSuchAccountError, я б підклас його, з Promise.OperationalErrorякого Bluebird вже передбачений. Якщо ви не знаєте, як підкласифікувати помилку, дайте мені знати.

Я б додатково підкласирував його для цього, AuthenticationErrorа потім зробив щось на кшталт:

function changePassword(queryDataEtc){ 
    return repository.Query(getAccountByIdQuery)
                     .then(convertDocumentToModel)
                     .then(verifyOldPassword)
                     .then(changePassword);
}

Як бачите - він дуже чистий, і ви можете прочитати текст, як інструкція з інструкцій, що відбувається в процесі. Він також відокремлений від запиту / відповіді.

Тепер я б назвав це від обробника маршруту як такого:

 changePassword(params)
 .catch(NoSuchAccountError, function(e){
     res.status(404).send({ error: "No account found with this Id" });
 }).catch(AuthenticationError, function(e){
     res.status(406).send({ OldPassword: error });
 }).error(function(e){ // catches any remaining operational errors
     res.status(500).send({ error: "Unable to change password" });
 }).catch(function(e){
     res.status(500).send({ error: "Unknown internal server error" });
 });

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


11
Ви можете додати, що причина виникнення проміжного .catch(someSpecificError)обробника для якоїсь конкретної помилки полягає в тому, якщо ви хочете виявити певний тип помилки (що нешкідливо), вирішіть її і продовжуйте наступний потік. Наприклад, у мене є якийсь стартовий код, який має послідовність дій. Перше, що потрібно прочитати конфігураційний файл з диска, але якщо цей конфігураційний файл відсутній, це помилка ОК (програма має вбудовані параметри за замовчуванням), щоб я міг обробляти цю конкретну помилку і продовжувати решту потоку. Також може бути прибирання, краще не залишати його пізніше.
jfriend00

1
Я подумав, що "Це половина пункту .catch - вміти відновлюватися від помилок" зробив це зрозумілим, але дякую за уточнення далі, це хороший приклад.
Бенджамін Грюнбаум

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

3
@clocksmith з ES6 обіцяє, що ви затримаєтеся, що все instanceofловите і робите chceks вручну.
Бенджамін Грюнбаум

1
Для тих, хто шукає посилання на субкласифікацію об'єктів помилок, читайте bluebirdjs.com/docs/api/catch.html#filtered-catch . Стаття також в значній мірі відтворює відповідь на кілька виловів, наведену тут.
mummybot

47

.catchпрацює на зразок try-catchзаяви, що означає, що вам потрібен лише один улов в кінці:

repository.Query(getAccountByIdQuery)
        .then(convertDocumentToModel)
        .then(verifyOldPassword)
        .then(changePassword)
        .then(function(){
            res.status(200).send();
        })
        .catch(function(error) {
            if (/*see if error is not found error*/) {
                res.status(404).send({ error: "No account found with this Id" });
            } else if (/*see if error is verification error*/) {
                res.status(406).send({ OldPassword: error });
            } else {
                console.log(error);
                res.status(500).send({ error: "Unable to change password" });
            }
        });

1
Так, я знав про це, але я не хотів робити величезний ланцюжок помилок, і мені здавалося, що це зручніше робити це як і коли це потрібно. Звідси виходить все в кінці, але мені подобається думка про введені помилки, що є більш описовим щодо наміру.
Грофіт

8
@Grofit за те, що варто вводити улов у Bluebird, був ідеєю Петка (Esailija) для початку :) Не потрібно переконувати його, що вони тут кращі. Я думаю, він не хотів вас бентежити, оскільки багато людей в JS не дуже обізнані з цією концепцією.
Бенджамін Груенбаум

17

Мені цікаво, чи є спосіб, щоб я якось змусив ланцюг зупинитися в певний момент на основі помилок

Ні. Ви дійсно не можете "закінчити" ланцюжок, якщо ви не кинете виняток, який бульбашки до кінця. Дивіться відповідь Бенджаміна Грюнбаума, як це зробити.

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

або якщо є кращий спосіб структурувати це, щоб отримати певну форму розгалуженої поведінки

Так, ви можете зробити розгалуження обіцянками . Однак це означає залишити ланцюг і "повернутися" до гніздування - так само, як ви це робите в вкладеному if-else або try-catch:

repository.Query(getAccountByIdQuery)
.then(function(account) {
    return convertDocumentToModel(account)
    .then(verifyOldPassword)
    .then(function(verification) {
        return changePassword(verification)
        .then(function() {
            res.status(200).send();
        })
    }, function(verificationError) {
        res.status(406).send({ OldPassword: error });
    })
}, function(accountError){
    res.status(404).send({ error: "No account found with this Id" });
})
.catch(function(error){
    console.log(error);
    res.status(500).send({ error: "Unable to change password" });
});

5

Я вчинив так:

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

    repository.Query(getAccountByIdQuery)
    .then((resultOfQuery) => convertDocumentToModel(resultOfQuery)) //inside convertDocumentToModel() you check for empty and then throw new Error('no_account')
    .then((model) => verifyOldPassword(model)) //inside convertDocumentToModel() you check for empty and then throw new Error('no_account')        
    .then(changePassword)
    .then(function(){
        res.status(200).send();
    })
    .catch((error) => {
    if (error.name === 'no_account'){
        res.status(404).send({ error: "No account found with this Id" });

    } else  if (error.name === 'wrong_old_password'){
        res.status(406).send({ OldPassword: error });

    } else {
         res.status(500).send({ error: "Unable to change password" });

    }
});

Ваші інші функції, мабуть, виглядатимуть приблизно так:

function convertDocumentToModel(resultOfQuery) {
    if (!resultOfQuery){
        throw new Error('no_account');
    } else {
    return new Promise(function(resolve) {
        //do stuff then resolve
        resolve(model);
    }                       
}

4

Напевно, трохи пізно на вечірку, але можна гніздитися, .catchяк показано тут:

Мережа розробників Mozilla - використання обіцянок

Редагувати: я подав це, оскільки він забезпечує задану функціональність загалом. Однак у цьому конкретному випадку це не відбувається. Тому що, як уже детально пояснили інші, .catchпомилка повинна відновити. Наприклад, ви не можете надсилати відповідь клієнтові в декількох .catch зворотних дзвінках, тому що .catchбез явного return вирішення це не відбувається undefinedв такому випадку, що призводить .thenдо запуску, навіть якщо ваша ланцюг насправді не вирішена, що може призвести .catchдо запуску та надсилання наступних чергова відповідь клієнту, що викликає помилку і, ймовірно, кидає UnhandledPromiseRejectionваш шлях. Я сподіваюся, що це заплутане речення мало для вас сенс.


1
@AntonMenshov Ви маєте рацію. Я розширив свою відповідь, пояснивши, чому його бажана поведінка все ще не можлива за допомогою гніздування
denkquer

2

Замість .then().catch()...вас можна зробити .then(resolveFunc, rejectFunc). Цей ланцюжок обіцянок буде кращим, якби ви вирішили речі по дорозі. Ось як я б це переписав:

repository.Query(getAccountByIdQuery)
    .then(
        convertDocumentToModel,
        () => {
            res.status(404).send({ error: "No account found with this Id" });
            return Promise.reject(null)
        }
    )
    .then(
        verifyOldPassword,
        () => Promise.reject(null)
    )
    .then(
        changePassword,
        (error) => {
            if (error != null) {
                res.status(406).send({ OldPassword: error });
            }
            return Promise.Promise.reject(null);
        }
    )
    .then(
        _ => res.status(200).send(),
        error => {
            if (error != null) {
                console.error(error);
                res.status(500).send({ error: "Unable to change password" });
            }
        }
    );

Примітка. Це if (error != null)трохи хак для взаємодії з останньою помилкою.


1

Я думаю , що відповідь Бенджаміна Грюнбаума вище - найкраще рішення для складної послідовності логіки, але ось моя альтернатива для більш простих ситуацій. Я просто використовую errorEncounteredпрапор разом з, return Promise.reject()щоб пропустити будь-які подальші thenабо catchзаяви. Так би виглядало так:

let errorEncountered = false;
someCall({
  /* do stuff */
})
.catch({
  /* handle error from someCall*/
  errorEncountered = true;
  return Promise.reject();
})
.then({
  /* do other stuff */
  /* this is skipped if the preceding catch was triggered, due to Promise.reject */
})
.catch({
  if (errorEncountered) {
    return;
  }
  /* handle error from preceding then, if it was executed */
  /* if the preceding catch was executed, this is skipped due to the errorEncountered flag */
});

Якщо у вас є більше двох пар / ловити, ви, ймовірно, повинні використовувати рішення Бенджаміна Грюнбаума. Але це працює для простої настройки.

Зауважте, що фінал catchмає return;радше, ніж return Promise.reject();тому, що наступних thenнам не потрібно пропускати, і це вважатиметься непорушеним відхиленням Обіцяння, що Вузлу не подобається. Як написано вище, фінал catchповерне мирно вирішене Обіцяння.

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