Обработка нескольких уловов в цепочке обещаний
Я до сих пор довольно новичок в 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
Решение, которое я недавно нашел и нашел очень удобным, заключается в следующем:
- Обрабатывать ошибку локально в блоке catch (
res.send(...)
), - вернуть
Promise.reject()
, - в последнем
.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" });
});