Gestione di più catture nella catena delle promesse


125

Sono ancora abbastanza nuovo alle promesse e attualmente sto usando bluebird, 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 cerco è:

  • Va a ottenere l'account dall'ID
  • Se c'è un rifiuto a questo punto, bombarda e restituisci un errore
  • Se non ci sono errori, convertire il documento restituito in un modello
  • Verificare la password con il documento del database
  • Se le password non corrispondono, bombardare e restituire un errore diverso
  • Se non ci sono errori, modificare le password
  • Quindi restituisci il successo
  • Se qualcos'altro è andato storto, restituisci un 500

Quindi attualmente le catture 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 esiste un modo migliore strutturarlo per ottenere una qualche forma di comportamento di ramificazione, come nel caso di if X do Y else Z.

Qualsiasi aiuto sarebbe grande.


Puoi rilanciare o tornare presto?
Pieter21

Risposte:


126

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 .catch: essere in grado di recuperare dagli errori. Potrebbe essere opportuno lanciare nuovamente 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à rilevato da un gestore successivo. Il vero problema qui è che i gestori di errori generalizzati "HANDLE ANYTHING" sono una cattiva 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 aggiuntivo è che la logica aziendale non deve (e non dovrebbe) essere a conoscenza del ciclo di richiesta / risposta. Non è responsabilità della query decidere quale stato HTTP e quale errore riceve il client e in seguito, man mano che 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.

Per prima cosa, dovrei .Querylanciare un NoSuchAccountError, lo sottoclasserei da quello Promise.OperationalErrorche Bluebird già fornisce. Se non sei sicuro di come sottoclassare un errore fammelo sapere.

Inoltre lo sottoclasserei per AuthenticationErrore poi farei 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 della rotta come 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 è tutta in un posto e la decisione su come gestire gli errori per il cliente è tutta in un posto e non si ingombrano a vicenda.


11
Potresti aggiungere che il motivo per avere un .catch(someSpecificError)gestore intermedio per qualche errore specifico è se vuoi rilevare un tipo specifico di errore (che è innocuo), affrontarlo e continuare il flusso che segue. Ad esempio, ho un codice di avvio che ha una sequenza di cose da fare. La prima cosa è leggere il file di configurazione dal disco, ma se quel file di configurazione manca è un errore OK (il programma ha impostazioni predefinite incorporate), quindi posso gestire quell'errore specifico e continuare il resto del flusso. Potrebbe anche esserci una pulizia migliore per non partire più tardi.
jfriend00

1
Ho pensato che "Questo è metà del punto di .catch - essere in grado di recuperare dagli errori" lo rendesse chiaro, ma grazie per aver chiarito ulteriormente questo è un buon esempio.
Benjamin Gruenbaum,

1
Cosa succede se bluebird non viene utilizzato? Le promesse semplici di es6 hanno solo un messaggio di errore di stringa che viene passato a catch.
orologiaio

3
@clocksmith con ES6 promette che sei bloccato a catturare tutto e fare i instanceofcontrolli manualmente da solo.
Benjamin Gruenbaum

1
Per coloro che cercano un riferimento per la creazione di sottoclassi di oggetti Error, leggere bluebirdjs.com/docs/api/catch.html#filtered-catch . L'articolo riproduce anche praticamente la risposta di cattura multipla fornita qui.
mummybot

47

.catchfunziona come l' try-catchaffermazione, il che significa che hai solo bisogno di una presa 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" });
            }
        });

1
Sì, lo sapevo, ma non volevo fare un'enorme catena di errori, e sembrava più leggibile farlo come e quando ne aveva bisogno. Da qui il trucco alla fine, ma mi piace l'idea degli errori di battitura in quanto è più descrittivo rispetto all'intento.
Grofit

8
@ Guadagno per quello che vale - le catture digitate in Bluebird erano l' idea di Petka (Esailija) per cominciare :) Non c'è bisogno di convincerlo che sono un approccio preferibile qui. Penso che non volesse confonderti dal momento che molte persone in JS non sono molto consapevoli del concetto.
Benjamin Gruenbaum

17

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

No. Non puoi davvero "terminare" una catena, a meno che non lanci un'eccezione che bolle fino alla sua fine. Vedi la risposta di Benjamin Gruenbaum per sapere come farlo.

Una derivazione del suo modello sarebbe non distinguere i tipi di errore, ma utilizzare errori che hanno statusCodee bodycampi che possono essere inviati da un singolo .catchgestore generico . A seconda della struttura dell'applicazione, la sua soluzione potrebbe essere più pulita.

o se esiste un modo migliore per strutturarlo per ottenere una qualche forma di comportamento di ramificazione

Sì, puoi fare ramificazioni con promesse . Tuttavia, questo significa lasciare la catena e "tornare" alla nidificazione, proprio come faresti in un'istruzione if-else o try-catch annidata:

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

Ho fatto in questo modo:

Alla fine lasci il tuo bottino. E lancia un errore quando accade 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 tue funzioni probabilmente sarebbero simili a questa:

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

4

Probabilmente un po 'in ritardo per la festa, ma è possibile nidificare .catchcome mostrato qui:

Mozilla Developer Network - Utilizzo delle promesse

Modifica: l'ho inviato perché fornisce la funzionalità richiesta in generale. Tuttavia non è così in questo caso particolare. Perché come già spiegato in dettaglio da altri, .catchsi suppone di recuperare l'errore. Non si può, ad esempio, inviare una risposta al cliente in più .catch callback perché un .catchsenza espliciti return risolve IT con undefinedin quel caso, causando procedere .thenal grilletto anche se la catena non è realmente risolto, causando potenzialmente un seguito .catchdi innesco e l'invio un'altra risposta al client, causando un errore e probabilmente lanciando una UnhandledPromiseRejectiontua strada. Spero che questa frase contorta abbia un senso per te.


1
@AntonMenshov Hai ragione. Ho ampliato la mia risposta, spiegando perché il suo comportamento desiderato non è ancora possibile con l'annidamento
denkquer

2

Invece di .then().catch()...te lo puoi fare .then(resolveFunc, rejectFunc). Questa catena di promesse sarebbe migliore 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: Il if (error != null)è un po 'di hack di interagire con l'errore più recente.


1

Penso che la risposta di Benjamin Gruenbaum sopra sia la soluzione migliore per una sequenza logica complessa, ma ecco la mia alternativa per situazioni più semplici. Uso solo un errorEncounteredflag insieme a return Promise.reject()per saltare qualsiasi istruzione theno successiva catch. Quindi sarebbe simile a questo:

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

Se hai più di due coppie then / catch, dovresti probabilmente usare la soluzione di Benjamin Gruenbaum. Ma questo funziona per una configurazione semplice.

Nota che il finale catchha solo return;piuttosto che return Promise.reject();, perché non c'è un successivo thenche dobbiamo saltare, e conterebbe come un rifiuto di Promise non gestito, che a Node non piace. Come scritto sopra, la finale catchrestituirà una Promessa pacificamente risolta.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.