1
votes

Dois-je éviter d'essayer catch dans chaque async / wait sur Node js?

C'est une question de conception qui m'est venue lors des tests unitaires. Plongeons dans l'exemple:

Imaginez ceci:

describe('testing bar', () => {
    it('foo should throw', () => {
        foo.mockImplementantion(() => { throw new CustomError('error')});
        bar()
        .then((result) => console.log(result))
        .catch((err) => { exepect(err).toBeInstanceOf(CustomError)}) // this is what we are testing
    })
})

Que voyons-nous ici? La seule fonction qui n'a pas de bloc try-catch est bar. Mais si foo échoue, il devrait être attrapé par la capture principale.

En unittesting ceci comme

async function foo() {
    try {
        return apiCall()
    }
    catch (e) {
        throw new CustomError(e);
    } 
}



async function bar() {
    return foo()
}



async function main() {
    try {
        await bar()
    }catch(e) {
        console.error(e)
    }
}

main()

Le résultat que nous voyons est qu'un Le rejet de promesse non géré est enregistré dans la console.

Donc, ma question est ... même si je sais que le main () détectera l'erreur, devrait J'utilise le bloc try-catch dans toutes les fonctions asynchrones?


5 commentaires

Étant donné qu'un processus node.js peut / se terminera à partir d'un rejet de promesse non géré, il est conseillé de toujours utiliser try / catch avec await et d'ajouter toujours un gestionnaire .catch à la fin des chaînes Promise.


ce que vous me dites, c'est que je devrais envelopper la fonction bar () dans un bloc try catch, non?


La fonction asynchrone bar renvoie une promesse non? La fonction async foo peut lever une exception dans son bloc catch qui provoque un rejet de promesse non gérée dans bar , donc ....... .


Votre scénario de test ne retourne pas la promesse ou n'utilise async / wait et il n'a pas de rappel terminé , il est donc probable que expect lève une exception concernant l'appel après la fin du test. Quel est le message d'erreur du rejet non géré?


@Bergi (nœud: 44872) UnhandledPromiseRejectionWarning: CustomError


3 Réponses :


2
votes

try..catch peut être nécessaire si une fonction est capable de récupérer d'une erreur, de faire un effet secondaire comme la journalisation ou de renvoyer une erreur plus significative.

Si CustomError est plus préférable qu'une erreur que apiCall peut lancer puis try..catch nécessaire, sinon ce n'est pas le cas. Le problème avec foo est qu'il ne gère que les erreurs synchrones. Afin de gérer les promesses rejetées, il devrait être return await apiCall () , c'est un piège connu de async.

Les rejets non interceptés sont indésirables, ils entraînent actuellement UnhandledPromiseRejectionWarning et devraient planter Node dans les versions futures. Il est préférable de gérer une erreur de manière significative au plus haut niveau, donc main doit attraper l'erreur. Cela peut être délégué au gestionnaire d'événements process uncaughtRejection , mais il peut être avantageux pour lui de conserver un niveau supplémentaire de gestion des erreurs qui ne devrait jamais être atteint.

Le résultat que nous voyons est qu'un rejet de promesse non gérée est enregistré dans la console.

Cela ne devrait pas arriver. Un rejet doit être traité par le test. Un point d'échec possible est expliqué ci-dessus, foo peut renvoyer l'erreur d'origine de apiCall au lieu de CustomError au cas où il n'aurait pas été correctement simulé, ceci échouera à l'attente et entraînera un rejet non géré dans catch () . Un autre point d'échec est que le test a une promesse déchaînée car il n'a pas été retourné, le test réussit toujours.

Le test asynchrone qui utilise des promesses doit toujours renvoyer une promesse. Cela peut être amélioré en utilisant async..await . foo est async , il devrait toujours renvoyer une promesse:

it('foo should throw', async () => {
    foo.mockImplementantion(() => { return Promise.reject(new CustomError('error')) });
    await expect(bar()).rejects.toThrow(CustomError);
})

Maintenant, même si foo code> mock échoue ( foo mock n'affectera pas bar s'ils sont définis dans le même module que celui indiqué) et bar rejette avec quelque chose qui n'est pas CustomError , cela sera affirmé.


3 commentaires

J'ai trois commentaires à ce sujet 1. try..catch peut être nécessaire si une fonction est capable de récupérer d'une erreur, de faire un effet secondaire comme la journalisation ou de relancer une erreur plus significative. C'est vrai, cela n'a pas d'importance dans ce cas 2. Je suis presque sûr que vous ne pouvez pas faire return await .... vous devriez simplement utiliser return 3. J'ai changé du code dans le test et je passe


@JulianMendez return await est certainement possible, et même nécessaire dans les blocs try


@JulianMendez Alors après tout, c'était juste le cas de test en train d'être cassé? Pouvez-vous nous montrer comment vous l'avez résolu?



0
votes

Comme @Bergi me l'a dit, je publierai quelques solutions ici

​​J'emballe la fonction dans un bloc try catch

1.

describe('testing bar', () => {
    it('foo should throw', () => {
        foo.mockImplementantion(() => { throw new CustomError('error')});
        return bar()
        .then((result) => { throw result })
        .catch((err) => { exepect(err).toBeInstanceOf(CustomError)}) // this is what we are testing
    })
})
  1. Réécrire le test
describe('testing bar', () => {
    it('foo should throw', (done) => {
        foo.mockImplementantion(() => { throw new CustomError('error')});
        bar()
        .then((result) => { throw result }) // this is because we are expecting an error, so if the promise resolves it's actually a bad sign.
        .catch((err) => { 
          exepect(err).toBeInstanceOf(CustomError)}) // this is what we are testing
          done();
    })
})
  1. Utiliser return dans le scénario de test
async function bar() {
    try{
       return foo()
    } catch (e) {
       throw e
    }
}



0
votes

Non. Vous n'avez pas besoin d'utiliser try / catch dans chaque async / await. Vous n'avez besoin que de le faire au plus haut niveau. Dans ce cas, la fonction principale que vous utilisez déjà.

La météo que vous devriez est une question d'opinion. Les concepteurs du langage go sont suffisamment convaincus à ce sujet qui est devenu la norme dans go pour toujours gérer les erreurs à chaque appel de fonction. Mais ce n'est pas la norme en javascript ou dans la plupart des autres langages.

Rejet de promesse non gérée

Votre rejet de promesse non gérée est déclenché par votre fonction it () car vous ne lui dites pas d'attendre la fin de la promesse.

Je suppose que vous utilisez quelque chose comme mocha pour le test unitaire (d'autres frameworks peuvent fonctionner différemment). Dans mocha, il existe deux façons de gérer les tests asynchrones:

  1. Appelez le rappel done - la fonction it () sera toujours appelée avec un rappel done . C'est à vous de savoir si vous voulez l'utiliser ou comme dans votre code posté pour ne pas l'utiliser:

     describe('testing bar', () => {
         it('foo should throw', (done) => {
             foo.mockImplementantion(() => { throw new CustomError('error')});
    
             return bar() // <----------- THIS WOULD ALSO FIX IT
             .then((result) => {
                 console.log(result);
              })
             .catch((err) => {
                 exepect(err).toBeInstanceOf(CustomError);
             })
         })
     })
    
  2. Renvoyez une promesse. Si vous renvoyez une promesse à la fonction it () , mocha sera conscient que votre code est asynchrone et attendra la fin:

     describe('testing bar', () => {
         it('foo should throw', (done) => {
             foo.mockImplementantion(() => { throw new CustomError('error')});
             bar()
             .then((result) => {
                 console.log(result);
                 done(); // ------------- THIS IS YOUR ACTUAL BUG
              })
             .catch((err) => {
                 exepect(err).toBeInstanceOf(CustomError);
                 done(); // ------------- THIS IS YOUR ACTUAL BUG
             })
         })
     })
    

En bref, il n'y a rien de mal avec votre code. Mais vous avez un bogue dans votre test unitaire .


3 commentaires

Notez que cette réponse ne fait que reformuler ce que @Bergi essaie de vous dire dans les commentaires. Votre réaction à son commentaire me dit que vous n'avez pas compris ce qu'il essayait de vous dire (non, vous n'avez pas besoin de modifier votre code - modifier votre test unitaire) alors j'ai élaboré.


Oh oui, j'ai compris ce que @Bergi m'a dit en deux. Il y a beaucoup de solutions à mon problème et je pense la même chose que vous: je n'ai pas besoin de bloc try / catch dans chaque async / await que j'utilise. Je publierai une autre solution au problème


C'est Jest mais cela fonctionne de la même manière que Mocha à cet égard. 1 pourrait être ignoré car il montre ce qui ne va pas avec done dans les promesses. La plupart du temps, un développeur n'a pas assez de discipline pour garantir un flux de contrôle correct. Dans ce cas, done après une assertion entraînera un délai d'expiration du test car il n'est jamais appelé lorsqu'une assertion échoue. Cela devrait être .then (() => done (), done.fail) comme dernière chaîne pour couvrir toutes les bases ... sauf que cela ne devrait pas parce que cela est déjà fait quand un la promesse est retournée comme indiqué en 2. TL; DR: n'utilisez pas done avec les promesses.