Gestire più catture nella catena di promise

Sono ancora abbastanza nuovo alle promesse e sto usando bluebird al momento, tuttavia ho uno scenario in cui non sono abbastanza sicuro di come gestirlo al meglio.

Quindi, ad esempio, ho una catena di promesse all’interno di un’app express in questo modo:

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

Quindi il comportamento che sto cercando è:

  • Va per ottenere un account da Id
  • Se a questo punto c’è un rifiuto, bombardare e restituire un errore
  • Se non ci sono errori, converti il ​​documento restituito a un modello
  • Verifica la password con il documento del database
  • Se le password non corrispondono, bombardare e restituire un errore diverso
  • Se non ci sono errori, cambia le password
  • Quindi restituire il successo
  • Se qualcos’altro è andato storto, restituisci un 500

Quindi le catture al momento non sembrano fermare il concatenamento, e questo ha senso, quindi mi chiedo se c’è un modo per me di forzare in qualche modo la catena a fermarsi a un certo punto in base agli errori, o se c’è un modo migliore strutturare questo per ottenere una qualche forma di comportamento ramificato, in quanto vi è un caso in cui if X do Y else Z

Qualsiasi aiuto sarebbe grande.

Questo comportamento è esattamente come un lancio sincrono:

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

Questo è metà del punto di .catch – per essere in grado di recuperare dagli errori. Potrebbe essere desiderabile ripetere il test per segnalare che lo stato è ancora un errore:

 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 

Tuttavia, questo da solo non funzionerà nel tuo caso poiché l’errore verrà catturato da un gestore successivo. Il vero problema qui è che i gestori di errori generalizzati di “HANDLE ANYTHING” sono una ctriggers pratica in generale e sono estremamente disapprovati in altri linguaggi di programmazione ed ecosistemi. Per questo motivo Bluebird offre catture tipizzate e predicate.

Il vantaggio aggiunto è che la logica aziendale non deve (e non dovrebbe) essere consapevole del ciclo richiesta / risposta. Non è responsabilità della query decidere quale stato HTTP e quale errore il client ottiene e più tardi quando la tua app cresce potresti voler separare la logica di business (come interrogare il tuo DB e come elaborare i tuoi dati) da ciò che invii al client (quale codice di stato http, quale testo e quale risposta).

Ecco come scriverei il tuo codice.

In primo luogo, mi piacerebbe ottenere .Query per lanciare un NoSuchAccountError , lo Promise.OperationalError da Promise.OperationalError che Bluebird già fornisce. Se non sei sicuro di come creare una sottoclass di errore, fammelo sapere.

Avrei anche sottoclassi per AuthenticationError e poi fare qualcosa come:

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

Come puoi vedere, è molto pulito e puoi leggere il testo come un manuale di istruzioni di ciò che accade nel processo. È anche separato dalla richiesta / risposta.

Ora, lo chiamerei dal gestore del percorso in quanto tale:

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

In questo modo, la logica è tutto in un unico posto e la decisione su come gestire gli errori per il cliente è tutto in un unico posto e non si confondono a vicenda.

.catch funziona come l’istruzione try-catch , il che significa che hai solo bisogno di una cattura alla fine:

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

Mi chiedo se ci sia un modo per me di forzare in qualche modo la catena a fermarsi a un certo punto in base agli errori

No. Non puoi davvero “terminare” una catena, a meno che non lanci un’eccezione che bolle fino alla fine. Vedi la risposta di Benjamin Gruenbaum su come farlo.

Una derivazione del suo modello non dovrebbe distinguere i tipi di errore, ma utilizzare gli errori che hanno campi statusCode e body che possono essere inviati da un singolo gestore di .catch . Tuttavia, a seconda della struttura dell’applicazione, la sua soluzione potrebbe essere più pulita.

o se c’è un modo migliore per strutturare questo per ottenere una qualche forma di comportamento ramificato

Sì, puoi fare ramamenti con le promesse . Tuttavia, questo significa lasciare la catena e “tornare indietro” alla nidificazione, proprio come faresti in una dichiarazione nidificata if-else o 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" }); }); 

Ho fatto così:

Lascerai il pescato alla fine. E fai un errore quando succede a metà della tua catena.

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

Le altre funzioni probabilmente assomigliano a questo:

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

Invece di .then().catch()... si può fare .then(resolveFunc, rejectFunc) . Questa catena di promesse sarebbe meglio se gestissi le cose lungo la strada. Ecco come lo riscriverei:

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

Nota: if (error != null) è un po ‘un hack per interagire con l’errore più recente.