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


125

Я все еще новичок в 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" });
        });

Итак, поведение, которое мне нужно, это:

  • Идет получить аккаунт по 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 ANYTHING» - плохая практика в целом и крайне не одобряются другими языками программирования и экосистемами. По этой причине 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 - иметь возможность восстанавливать после ошибок» ясно дает понять, но спасибо за пояснение, это хороший пример.
Benjamin Gruenbaum

1
Что делать, если bluebird не используется? Простые обещания es6 содержат только строковое сообщение об ошибке, которое передается для перехвата.
clocksmith

3
@clocksmith с ES6 обещает, что вы застряли в instanceofловушке всего и самостоятельно выполняете chceks.
Бенджамин Грюнбаум

1
Для тех, кто ищет ссылку для создания подклассов объектов Error, прочтите 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
Да, я знал об этом, но я не хотел создавать огромную цепочку ошибок, и казалось более читаемым, делать это по мере необходимости. Отсюда и загвоздка в конце, но мне нравится идея типизированных ошибок, поскольку это более информативно в отношении цели.
Grofit

8
@Grofit, чего стоит - набранные уловы в Bluebird были идеей Петьки (Есаилия) для начала :) Не нужно убеждать его, что они здесь предпочтительнее. Я думаю, он не хотел вас сбивать с толку, поскольку многие люди в 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 обратных вызовах, потому что a .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()чтобы пропустить любые последующие операторы thenor 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вернет мирно разрешенное обещание.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.