Обработка нескольких уловов в цепочке обещаний

Я до сих пор довольно новичок в promises и сейчас использую bluebird, однако у меня есть сценарий, в котором я не совсем уверен, как наилучшим образом справиться с этим.

Так, например, у меня есть цепочка обещаний в экспресс-приложении, например:

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

Итак, поведение, за которым я следую, следующее:

  • Идет, чтобы получить учетную запись по идентификатору
  • Если в этот момент есть отклонение, выполните вскрытие и верните ошибку.
  • Если нет ошибки, конвертируйте документ, возвращенный в модель
  • Проверьте пароль с документом базы данных
  • Если пароли не совпадают, тогда вылетают и возвращают другую ошибку.
  • Если нет ошибки, пароли
  • Затем верните успех
  • Если что-то еще пошло не так, верните 500

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

Любая помощь будет большой.

Ответ 1

Это поведение в точности как синхронный бросок:

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 ANYTHING" в целом являются плохой практикой и крайне недовольны другими языками программирования и экосистемами. По этой причине Bluebird предлагает типизированные и предикатные уловы.

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

Вот как я бы написал ваш код.

Во-первых, я бы получил .Query для NoSuchAccountError, я бы 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" });
 });

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

Ответ 2

.catch работает как оператор try-catch, что означает, что вам нужно только один 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" });
            }
        });

Ответ 3

Мне интересно, есть ли способ заставить меня каким-то образом заставить цепочку остановиться в определенной точке на основе ошибок

Нет. Вы не можете "закончить" цепочку, если только вы не выбросите исключение, которое пузырится до конца. См. ответ Бенджамина Грюнбаума о том, как это сделать.

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

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

Да, вы можете сделать ветвление с promises. Тем не менее, это означает, что вы оставите цепочку и "вернитесь" к вложенности - точно так же, как вы делали бы в вложенном выражении 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" });
});

Ответ 4

Я делал так:

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

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

Ответ 5

Вместо .then().catch()... .then(resolveFunc, rejectFunc) .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) - это хакерское взаимодействие с самой последней ошибкой.

Ответ 6

Я думаю, что ответ Бенджамина Грюнбаума, приведенный выше, является лучшим решением для сложной логической последовательности, но вот моя альтернатива для более простых ситуаций. Я просто использую флаг 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 */
});

Если у вас более двух пар then/catch, вам, вероятно, следует использовать раствор Бенджамина Грюнбаума. Но это работает для простой настройки.

Обратите внимание, что последний catch имеет только return; вместо того, чтобы return Promise.reject(); , Потому что там нет Последующий then, что нам нужно пропустить, и это будет засчитываться как необработанное отказ Promise, который Node не любит. Как написано выше, окончательный catch вернет мирно решенное обещание.

Ответ 7

Решение, которое я недавно нашел и нашел очень удобным, заключается в следующем:

  1. Обрабатывать ошибку локально в блоке catch (res.send(...)),
  2. вернуть Promise.reject(),
  3. в последнем .catch проверьте, существует ли ошибка; если нет, ничего не делать (уже обработано)

Так что вам не нужно объявлять пользовательский объект ошибки и делать большие изменения в конце. Вот так:

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