Explorer les principes de propagation des erreurs
L’un des concepts les plus discutables en JavaScript et TypeScript est la propagation des erreurs. Une partie du problème est l’incompréhension de la différence entre une exception et une erreur.
L’article vise à découvrir les types d’exceptions et à définir les principes de propagation des erreurs. Même si l’article met l’accent sur JavaScript et TypeScript, les mêmes principes de propagation des erreurs s’appliquent à de nombreux autres langages.
Une erreur est un objet contenant des informations sur ce qui s’est mal passé et où cela s’est produit dans le code. Les exceptions ne sont pas des erreurs ; exceptions sont des conditions anormales ou exceptionnelles nécessitant un traitement spécial. Il existe deux types de telles conditions : opérationnel et non opérationnelle.
Les erreurs de validation des entrées sont opérationnel les erreurs. Une tentative de connexion infructueuse est un opérationnel situation. Ces cas d’utilisation sont attendus et traités en conséquence. L’application continue de fonctionner comme d’habitude chaque fois que de tels scénarios se produisent.
Un non opérationnel condition est lorsqu’une application ne peut pas résoudre automatiquement une erreur et doit être fermée. Par exemple, une application doit stocker des données dans une base de données. Certaines fonctionnalités de l’application sont perdues lorsque la connexion à une base de données est impossible. Si cette fonctionnalité est critique, l’application est dans un non opérationnelle Etat. S’il ne peut pas récupérer automatiquement, il doit être arrêté.
Tout part de la propagation des erreurs. La méthode avec laquelle une erreur est renvoyée détermine si l’application peut continuer à fonctionner ou s’il vaut mieux s’arrêter. La méthode de propagation d’erreur définit également la manière dont l’erreur doit être gérée.
Il existe deux manières de propager une erreur en JavaScript et TypeScript :
- Jeter une exception. Il met fin au processus s’il n’est pas géré. Il doit être utilisé lorsque l’intention est d’arrêter une application en cas de problème.
- Renvoie une erreur. Il dénote un scénario de cas d’erreur attendu et ne nécessite pas l’arrêt de l’application.
Le danger d’utiliser le jeter mécanisme pour renvoyer une erreur est qu’il peut mettre fin à une application s’il n’est pas géré correctement. Le simple fait de renvoyer des erreurs n’arrêtera pas une application. Mais ignorer de telles erreurs peut amener une application dans un état inattendu et désordonné.
Un autre aspect essentiel lors du choix d’un mécanisme de propagation d’erreur est la documentation. Lorsque vous travaillez avec TypeScript, le service de langage déduit les types de paramètres d’entrée et de sortie des fonctions. Si une fonction renvoie un paramètre de type erreur, il est visible dans l’éditeur de code et réduit le risque de manquer la gestion des erreurs. La situation est différente lorsqu’une fonction lève une exception — le service de langage ne peut pas l’identifier. Il y a le jette balise définie dans TSDoc, mais il n’est pas très utilisé. La seule façon de savoir si la fonction lève une exception est de lire son code source.
La gestion des exceptions dépend de la manière dont une erreur est renvoyée et du type de fonction qui renvoie une erreur. C’est relativement facile pour les fonctions synchrones. Erreur vérifiée avec si clause dans le cas où elle est retournée par une fonction. Si une fonction lève une exception, son appel est généralement protégé par essayer/attraper.
La gestion des exceptions est plus complexe dans le cas des fonctions asynchrones. Il existe plusieurs façons de gérer les opérations asynchrones :
- Utiliser les fonctions de rappel
- Utilisez les promesses avec
.then
et.catch
- Utilisation
await
résoudre les promesses
Il y a aussi des générateurs, mais je vais les omettre.
La gestion des exceptions dans les rappels est assez simple. Chaque fois qu’une opération asynchrone est terminée, le gestionnaire d’opération appelle une fonction de rappel et renvoie des erreurs s’il y en a. Par convention, le premier paramètre de la fonction callback est une erreur. Si une erreur n’est pas nulle, l’invocation de la fonction de rappel s’arrête. Cela rend le flux de contrôle facile à comprendre.
Il y a quelques défis ici. Il n’est pas clair si l’erreur résulte d’un opérationnel ou non opérationnelle exception. Un autre défi est le traitement de la propagation des résultats. La nécessité de transmettre la sortie d’une fonction à l’entrée d’une autre a créé beaucoup de code avec des rappels imbriqués, ce qui est difficile à lire et à maintenir.
Les promesses ont résolu le problème de l’orchestration des invocations séquentielles de fonctions (synchrones et asynchrones). Une séquence de promesses peut être jointe en un seul appel avec .alors, et les données peuvent circuler entre les appels. Mais les promesses utilisent des rappels et héritent des mêmes défis de gestion des exceptions.
Asynchrone/attendre supprimé le besoin d’utiliser des rappels et résolu le problème d’orchestration. Le résultat de la fonction asynchrone peut maintenant être attendu. Cela rend le code plus propre et plus facile à lire, un chemin heureux au moins. Essayer/attraper a remplacé la technique de gestion des erreurs de rappel. C’était comme une victoire au début. En réalité, de nouveaux problèmes ont remplacé les anciens problèmes :
- Lors de la garde d’un appel de fonction asynchrone avec essayer/attraper, on ne sait toujours pas si l’erreur résulte d’un opérationnel ou non opérationnelle exception.
- Il n’est pas rare de voir plusieurs appels de fonction asynchrones dans un même essayer/attraper bloquer. Dans ce cas, le attraper La clause gère les exceptions de toutes les fonctions. Il n’est pas rare non plus de voir une logique complexe de traitement des erreurs dans le attraper block pour tenter de comprendre quel appel de fonction est la source de l’erreur.
- Imbriqué essayer/attraper blocs. Cela semble encore pire que les rappels imbriqués.
Alors, comment pouvons-nous utiliser l’avantage de asynchrone/attendre dans l’orchestration des appels asynchrones et minimiser les problèmes de gestion des exceptions ?
- Erreur de retour pour opérationnel exceptions.
- Vérifiez toujours les erreurs renvoyées.
- Lancer une erreur pour non opérationnelle exceptions.
- Essayer Le bloc d’instructions doit protéger une seule unité logique ou un appel.
- Évitez la logique complexe dans un attraper clause.
- Ne niche pas essayer/attraper blocs.
- Erreurs d’emballage.
Envelopper les erreurs signifie prendre une valeur d’erreur et y mettre une autre valeur d’erreur. Il permet d’étendre une erreur avec des informations supplémentaires sur son origine ou comment elle s’est produite sans perdre la valeur d’origine. Il y a un peu bibliothèques prise en charge de l’emballage d’erreur dans JavaScript. Alternativement, vous pouvez adopter ECMAScript2022 qui a un support intégré de error.cause
.
Les sections suivantes montrent des exemples de propagation et de gestion des erreurs :
Essayez l’approche de capture
Il est facile de raisonner sur le code quand essayer/attraper garde une seule déclaration. Les choses empirent rapidement lorsqu’il est nécessaire de gérer plusieurs appels asynchrones.
Envelopper l’approche try/catch
Cette approche cache essayer/attraper au sein de la fonction. La déclaration de la fonction indique qu’elle peut renvoyer une erreur ou une valeur. Il permet d’utiliser un si déclaration pour contrôler le flux logique.
L’exemple suivant est la prochaine étape pour généraliser la gestion des erreurs. Il utilise des types d’utilitaires TypeScript pour déduire les paramètres de fonction et les types de sortie.
Les exemples ci-dessus utilisent Pas lire l’API de fichier. Pour les exécuter, utilisez la commande suivante :
$ FILE=hello.txt deno run --allow-env=FILE --allow-read main.ts