Vous trouverez ci-dessous un résumé et une sélection de nombreuses sources différentes sur ce sujet, y compris un exemple de code et des citations de certains articles de blog. La liste complète des meilleures pratiques se trouve ici
Meilleures pratiques de gestion des erreurs Node.JS
Number1: Utilisez des promesses pour la gestion des erreurs asynchrones
TL; DR: gestion des erreurs asynchrones dans le style de rappel est probablement le moyen le plus rapide d'enfer (alias la pyramide du destin). Le meilleur cadeau que vous puissiez faire à votre code est d'utiliser à la place une bibliothèque de promesses réputée qui fournit une syntaxe de code bien compacte et familière comme try-catch
Autrement: style de rappel Node.JS, la fonction (erreur, réponse), est un moyen prometteur de code non maintenable en raison de la combinaison de la gestion des erreurs avec du code occasionnel, de l'imbrication excessive et des modèles de codage maladroits
Exemple de code - bon
doWork()
.then(doWork)
.then(doError)
.then(doWork)
.catch(errorHandler)
.then(verify);
exemple de code anti modèle - gestion des erreurs de style de rappel
getData(someParameter, function(err, result){
if(err != null)
//do something like calling the given callback function and pass the error
getMoreData(a, function(err, result){
if(err != null)
//do something like calling the given callback function and pass the error
getMoreData(b, function(c){
getMoreData(d, function(e){
...
});
});
});
});
});
Citation du blog: "Nous avons un problème avec les promesses"
(extrait du blog pouchdb, classé 11 pour les mots clés "Node Promises")
"… Et en fait, les rappels font quelque chose de plus sinistre encore: ils nous privent de la pile, ce que nous tenons généralement pour acquis dans les langages de programmation. Écrire du code sans pile, c'est un peu comme conduire une voiture sans pédale de frein: vous ne réalisez pas à quel point vous en avez besoin, tant que vous ne l'avez pas atteint et qu'il n'est pas là. Le but des promesses est de nous rendre les bases linguistiques que nous avons perdues lorsque nous sommes allés en mode asynchrone: le retour, le lancer et la pile. Mais vous doivent savoir utiliser correctement les promesses afin d'en tirer parti. "
Number2: utilisez uniquement l'objet Error intégré
TL; DR: Il est assez courant de voir du code qui génère des erreurs sous forme de chaîne ou de type personnalisé - cela complique la logique de gestion des erreurs et l'interopérabilité entre les modules. Que vous rejetiez une promesse, leviez une exception ou émettiez une erreur, l'utilisation de l'objet d'erreur intégré Node.JS augmente l'uniformité et empêche la perte d'informations sur les erreurs
Sinon: lors de l'exécution d'un module, le fait de savoir quel type d'erreurs en retour est incertain - rend beaucoup plus difficile de raisonner sur l'exception à venir et de la gérer. Même utile, l'utilisation de types personnalisés pour décrire des erreurs peut entraîner la perte d'informations sur les erreurs critiques comme la trace de la pile!
Exemple de code - bien faire
//throwing an Error from typical function, whether sync or async
if(!productToAdd)
throw new Error("How can I add new product when no value provided?");
//'throwing' an Error from EventEmitter
const myEmitter = new MyEmitter();
myEmitter.emit('error', new Error('whoops!'));
//'throwing' an Error from a Promise
return new promise(function (resolve, reject) {
DAL.getProduct(productToAdd.id).then((existingProduct) =>{
if(existingProduct != null)
return reject(new Error("Why fooling us and trying to add an existing product?"));
exemple de code anti motif
//throwing a String lacks any stack trace information and other important properties
if(!productToAdd)
throw ("How can I add new product when no value provided?");
Citation du blog: "Une chaîne n'est pas une erreur"
(D'après le blog devthought, classé 6 pour les mots clés "Node.JS error object")
"… La transmission d'une chaîne au lieu d'une erreur entraîne une interopérabilité réduite entre les modules. Elle rompt les contrats avec les API qui pourraient effectuer des vérifications d'erreurs ou qui souhaitent en savoir plus sur l'erreur . Les objets d'erreur, comme nous le verrons, ont très propriétés intéressantes dans les moteurs JavaScript modernes en plus de contenir le message transmis au constructeur .. "
Numéro3: Distinguer les erreurs opérationnelles des erreurs de programmation
TL; DR: les erreurs d'opération (par exemple, l'API a reçu une entrée non valide) se réfèrent à des cas connus où l'impact de l'erreur est entièrement compris et peut être géré de manière réfléchie. D'autre part, une erreur de programmeur (par exemple, essayer de lire une variable non définie) fait référence à des échecs de code inconnus qui dictent de redémarrer l'application avec grâce.
Sinon: vous pouvez toujours redémarrer l'application lorsqu'une erreur apparaît, mais pourquoi laisser ~ 5000 utilisateurs en ligne en raison d'une erreur mineure et prédite (erreur opérationnelle)? l'inverse n'est pas non plus idéal - garder l'application en place lorsqu'un problème inconnu (erreur de programmation) s'est produit peut entraîner un comportement imprévu. Différencier les deux permet d'agir avec tact et d'appliquer une approche équilibrée en fonction du contexte donné
Exemple de code - bien faire
//throwing an Error from typical function, whether sync or async
if(!productToAdd)
throw new Error("How can I add new product when no value provided?");
//'throwing' an Error from EventEmitter
const myEmitter = new MyEmitter();
myEmitter.emit('error', new Error('whoops!'));
//'throwing' an Error from a Promise
return new promise(function (resolve, reject) {
DAL.getProduct(productToAdd.id).then((existingProduct) =>{
if(existingProduct != null)
return reject(new Error("Why fooling us and trying to add an existing product?"));
exemple de code - marquer une erreur comme opérationnelle (fiable)
//marking an error object as operational
var myError = new Error("How can I add new product when no value provided?");
myError.isOperational = true;
//or if you're using some centralized error factory (see other examples at the bullet "Use only the built-in Error object")
function appError(commonType, description, isOperational) {
Error.call(this);
Error.captureStackTrace(this);
this.commonType = commonType;
this.description = description;
this.isOperational = isOperational;
};
throw new appError(errorManagement.commonErrors.InvalidInput, "Describe here what happened", true);
//error handling code within middleware
process.on('uncaughtException', function(error) {
if(!error.isOperational)
process.exit(1);
});
Citation de blog : "Sinon, vous risquez l'état" (D'après le blog débogable, classé 3 pour les mots clés "Node.JS uncaught exception")
" ... De par la nature même du fonctionnement de throw en JavaScript, il n'y a presque jamais moyen de" reprendre là où vous vous étiez arrêté "en toute sécurité, sans divulguer de références, ou créer une autre sorte d'état fragile indéfini. La manière la plus sûre de répondre à une erreur renvoyée consiste à arrêter le processus . Bien sûr, dans un serveur Web normal, de nombreuses connexions peuvent être ouvertes, et il n'est pas raisonnable de les fermer brutalement car une erreur a été déclenchée par quelqu'un d'autre. La meilleure approche consiste à envoyer une réponse d'erreur à la demande qui a déclenché l'erreur, tout en laissant les autres terminer dans leur temps normal, et arrêter d'écouter les nouvelles demandes de ce travailleur "
Number4: Gérer les erreurs de manière centralisée, mais pas dans le middleware
TL; DR: la logique de gestion des erreurs, comme le courrier à l'administrateur et la journalisation, doit être encapsulée dans un objet dédié et centralisé que tous les points finaux (par exemple, middleware Express, tâches cron, tests unitaires) appellent lorsqu'une erreur survient.
Sinon: le fait de ne pas traiter les erreurs au sein d'un même emplacement entraînera une duplication de code et probablement des erreurs mal gérées.
Exemple de code - un flux d'erreur typique
//DAL layer, we don't handle errors here
DB.addDocument(newCustomer, (error, result) => {
if (error)
throw new Error("Great error explanation comes here", other useful parameters)
});
//API route code, we catch both sync and async errors and forward to the middleware
try {
customerService.addNew(req.body).then(function (result) {
res.status(200).json(result);
}).catch((error) => {
next(error)
});
}
catch (error) {
next(error);
}
//Error handling middleware, we delegate the handling to the centrzlied error handler
app.use(function (err, req, res, next) {
errorHandler.handleError(err).then((isOperationalError) => {
if (!isOperationalError)
next(err);
});
});
Citation du blog: "Parfois, les niveaux inférieurs ne peuvent rien faire d'utile sauf propager l'erreur à leur appelant" (D'après le blog Joyent, classé 1 pour les mots clés "Gestion des erreurs Node.JS")
"... Vous pouvez finir par gérer la même erreur à plusieurs niveaux de la pile. Cela se produit lorsque les niveaux inférieurs ne peuvent rien faire d'utile, sauf propager l'erreur à leur appelant, qui la propage à son appelant, et ainsi de suite. Souvent, seul l'appelant de niveau supérieur sait quelle est la réponse appropriée, que ce soit pour recommencer l'opération, signaler une erreur à l'utilisateur ou autre chose. Mais cela ne signifie pas que vous devez essayer de signaler toutes les erreurs à un seul niveau supérieur rappel, car ce rappel lui-même ne peut pas savoir dans quel contexte l'erreur s'est produite "
Number5: documenter les erreurs de l'API à l'aide de Swagger
TL; DR: laissez vos appelants API savoir quelles erreurs peuvent survenir en retour afin de pouvoir les traiter de manière réfléchie sans se bloquer. Cela se fait généralement avec les cadres de documentation de l'API REST comme Swagger
Sinon: un client API peut décider de planter et de redémarrer uniquement parce qu'il a reçu une erreur qu'il ne pouvait pas comprendre. Remarque: l'appelant de votre API peut être vous (très typique dans un environnement de microservices)
Citation du blog: "Vous devez dire à vos appelants quelles erreurs peuvent se produire" (tiré du blog Joyent, classé 1 pour les mots clés "Journalisation Node.JS")
… Nous avons parlé de la façon de gérer les erreurs, mais lorsque vous écrivez une nouvelle fonction, comment envoyez-vous des erreurs au code qui a appelé votre fonction? … Si vous ne savez pas quelles erreurs peuvent se produire ou si vous ne savez pas ce qu'elles signifient, votre programme ne peut être correct, sauf par accident. Donc, si vous écrivez une nouvelle fonction, vous devez dire à vos appelants quelles erreurs peuvent se produire et ce qu'elles
Number6: Arrêtez le processus gracieusement quand un étranger vient en ville
TL; DR: Lorsqu'une erreur inconnue se produit (une erreur de développeur, voir la meilleure pratique numéro 3) - il y a une incertitude sur la salubrité de l'application. Une pratique courante suggère de redémarrer le processus avec précaution à l'aide d'un outil de «redémarrage» comme Forever et PM2
Sinon: lorsqu'une exception inconnue est interceptée, un objet peut être dans un état défectueux (par exemple, un émetteur d'événements qui est utilisé globalement et ne déclenche plus d'événements en raison d'une défaillance interne) et toutes les demandes futures peuvent échouer ou se comporter de manière folle
Exemple de code - décider s'il faut planter
//deciding whether to crash when an uncaught exception arrives
//Assuming developers mark known operational errors with error.isOperational=true, read best practice #3
process.on('uncaughtException', function(error) {
errorManagement.handler.handleError(error);
if(!errorManagement.handler.isTrustedError(error))
process.exit(1)
});
//centralized error handler encapsulates error-handling related logic
function errorHandler(){
this.handleError = function (error) {
return logger.logError(err).then(sendMailToAdminIfCritical).then(saveInOpsQueueIfCritical).then(determineIfOperationalError);
}
this.isTrustedError = function(error)
{
return error.isOperational;
}
Citation du blog: "Il existe trois écoles de pensée sur la gestion des erreurs" (tiré du blog jsrecipes)
… Il existe principalement trois écoles de pensée sur la gestion des erreurs: 1. Laissez l'application se bloquer et redémarrez-la. 2. Gérez toutes les erreurs possibles et ne plantez jamais. 3. Approche équilibrée entre les deux
Number7: Utilisez un enregistreur mature pour augmenter la visibilité des erreurs
TL; DR: un ensemble d'outils de journalisation matures comme Winston, Bunyan ou Log4J, accélérera la découverte et la compréhension des erreurs. Oubliez donc console.log.
Sinon: le survol via console.logs ou manuellement via un fichier texte en désordre sans outils d'interrogation ou une visionneuse de journal décente peut vous occuper au travail jusqu'à tard
Exemple de code - Enregistreur Winston en action
//your centralized logger object
var logger = new winston.Logger({
level: 'info',
transports: [
new (winston.transports.Console)(),
new (winston.transports.File)({ filename: 'somefile.log' })
]
});
//custom code somewhere using the logger
logger.log('info', 'Test Log Message with some parameter %s', 'some parameter', { anything: 'This is metadata' });
Citation du blog: "Permet d'identifier quelques exigences (pour un enregistreur):" (Sur le blog strongblog)
… Permet d'identifier quelques exigences (pour un enregistreur): 1. Horodatage de chaque ligne de journal. Celui-ci est assez explicite - vous devriez pouvoir dire quand chaque entrée de journal s'est produite. 2. Le format d'enregistrement devrait être facilement assimilable par les humains ainsi que par les machines. 3. Permet plusieurs flux de destination configurables. Par exemple, vous pouvez écrire des journaux de trace dans un fichier, mais lorsqu'une erreur se produit, écrivez dans le même fichier, puis dans le fichier d'erreur et envoyez un e-mail en même temps…
Number8: Découvrez les erreurs et les temps d'arrêt en utilisant les produits APM
TL; DR: les produits de surveillance et de performance (aka APM) évaluent de manière proactive votre base de code ou votre API afin qu'ils puissent mettre en évidence automatiquement les erreurs, les plantages et les ralentissements qui vous manquaient
Sinon: vous pourriez consacrer beaucoup d'efforts à mesurer les performances et les temps d'arrêt de l'API, vous ne saurez probablement jamais quelles sont vos parties de code les plus lentes dans le scénario du monde réel et comment elles affectent l'UX
Citation du blog: "Segments de produits APM" (Extrait du blog Yoni Goldberg)
"… Les produits APM constituent 3 segments principaux: 1. Surveillance de sites Web ou d'API - services externes qui surveillent constamment la disponibilité et les performances via des requêtes HTTP. Peut être configuré en quelques minutes. Voici quelques candidats sélectionnés: Pingdom, Uptime Robot et New Relic
2 Instrumentation de code - famille de produits qui nécessitent d'incorporer un agent dans l'application pour bénéficier de la détection de code lent, des statistiques d'exceptions, de la surveillance des performances et bien d'autres encore. Voici quelques candidats sélectionnés: New Relic, App Dynamics
3. Tableau de bord de l'intelligence opérationnelle -ces gammes de produits sont axées sur la facilitation de l'équipe des opérations avec des métriques et du contenu organisé qui permettent de rester facilement au top des performances des applications. Cela implique généralement d'agréger plusieurs sources d'informations (journaux d'application, journaux de base de données, journal de serveurs, etc.) et de concevoir un tableau de bord initial. Voici quelques candidats sélectionnés: Datadog, Splunk "
Ce qui précède est une version abrégée - voir ici plus de bonnes pratiques et d'exemples