Gestion des exceptions de bonnes pratiques Node.js

755

Je viens de commencer à essayer node.js il y a quelques jours. J'ai réalisé que le nœud est terminé chaque fois que j'ai une exception non gérée dans mon programme. Ceci est différent du conteneur de serveur normal auquel j'ai été exposé, où seul le thread de travail meurt lorsque des exceptions non gérées se produisent et le conteneur serait toujours en mesure de recevoir la demande. Cela soulève quelques questions:

  • Est-ce process.on('uncaughtException')le seul moyen efficace de s'en prémunir?
  • Attrapera-t-il également process.on('uncaughtException')l'exception non gérée lors de l'exécution des processus asynchrones?
  • Existe-t-il un module déjà construit (comme l'envoi d'e-mails ou l'écriture dans un fichier) que je pourrais exploiter dans le cas d'exceptions non interceptées?

J'apprécierais tout pointeur / article qui me montrerait les meilleures pratiques courantes pour gérer les exceptions non capturées dans node.js

momo
la source
11
aucune exception non interceptée ne devrait se produire. S'ils utilisent un programme qui redémarre l'intégralité de votre application lors de son plantage (nodemon, forever, superviseur)
Raynos
116
Des exceptions non détectées peuvent toujours se produire à moins que vous ne mettiez chaque morceau de votre code asynchrone à l'intérieur try .. catch, et vérifiez que cela est également fait pour toutes vos bibliothèques
Dan
13
+1 Dan Au début, je pensais que toutes vos librairies étaient un peu exagérées, car vous n'avez "que" besoin d'envelopper tous vos "points d'entrée de threads" dans le code dans try / catches. Mais en y réfléchissant plus attentivement, n'importe quelle bibliothèque pourrait avoir un setTimeoutou setIntervalou quelque chose de ce genre enfoui quelque part en profondeur qui ne peut pas être capturé par votre code.
Eugene Beresovsky
8
@EugeneBeresovksy Dan a raison, mais cela ne change pas le fait que lorsque des exceptions uncaughtExceptions se produisent, la seule option sûre consiste à redémarrer l'application. En d'autres termes, votre application est tombée en panne et il n'y a rien que vous puissiez ou devez faire à ce sujet. Si vous voulez faire quelque chose de constructif, implémentez la nouvelle fonctionnalité de domaine v0.8, encore expérimentale, afin de pouvoir enregistrer le plantage et envoyer une réponse 5xx à votre client.
ostergaard
1
@Dan Même en incluant toutes les fonctions de rappel dans try .. catch ne garantit pas les erreurs de capture. Dans le cas où un module requis utilise ses propres fichiers binaires, ils peuvent se bloquer de manière disgracieuse. J'ai eu cela avec phantomjs-node, échouant sur des erreurs qui sont impossibles à détecter (sauf si je devais faire une sorte d'inspection de processus sur les binaires requis, mais je n'ai jamais poursuivi cela).
Trindaz

Réponses:

739

Mise à jour: Joyent a maintenant son propre guide . Les informations suivantes sont plutôt un résumé:

Erreurs de «lancement» en toute sécurité

Idéalement, nous aimerions éviter les erreurs non capturées autant que possible, en tant que tel, au lieu de jeter littéralement l'erreur, nous pouvons à la place "jeter" l'erreur en utilisant l'une des méthodes suivantes en fonction de notre architecture de code:

  • Pour le code synchrone, si une erreur se produit, renvoyez l'erreur:

    // Define divider as a syncrhonous function
    var divideSync = function(x,y) {
        // if error condition?
        if ( y === 0 ) {
            // "throw" the error safely by returning it
            return new Error("Can't divide by zero")
        }
        else {
            // no error occured, continue on
            return x/y
        }
    }
    
    // Divide 4/2
    var result = divideSync(4,2)
    // did an error occur?
    if ( result instanceof Error ) {
        // handle the error safely
        console.log('4/2=err', result)
    }
    else {
        // no error occured, continue on
        console.log('4/2='+result)
    }
    
    // Divide 4/0
    result = divideSync(4,0)
    // did an error occur?
    if ( result instanceof Error ) {
        // handle the error safely
        console.log('4/0=err', result)
    }
    else {
        // no error occured, continue on
        console.log('4/0='+result)
    }
  • Pour le code basé sur le rappel (c'est-à-dire asynchrone), le premier argument du rappel est err, si une erreur se produit errest l'erreur, si une erreur ne se produit pas, elle l' errest null. Tout autre argument suit l' errargument:

    var divide = function(x,y,next) {
        // if error condition?
        if ( y === 0 ) {
            // "throw" the error safely by calling the completion callback
            // with the first argument being the error
            next(new Error("Can't divide by zero"))
        }
        else {
            // no error occured, continue on
            next(null, x/y)
        }
    }
    
    divide(4,2,function(err,result){
        // did an error occur?
        if ( err ) {
            // handle the error safely
            console.log('4/2=err', err)
        }
        else {
            // no error occured, continue on
            console.log('4/2='+result)
        }
    })
    
    divide(4,0,function(err,result){
        // did an error occur?
        if ( err ) {
            // handle the error safely
            console.log('4/0=err', err)
        }
        else {
            // no error occured, continue on
            console.log('4/0='+result)
        }
    })
  • Pour le code mouvementé , où l'erreur peut se produire n'importe où, au lieu de lancer l'erreur, déclenchez l' errorévénement à la place :

    // Definite our Divider Event Emitter
    var events = require('events')
    var Divider = function(){
        events.EventEmitter.call(this)
    }
    require('util').inherits(Divider, events.EventEmitter)
    
    // Add the divide function
    Divider.prototype.divide = function(x,y){
        // if error condition?
        if ( y === 0 ) {
            // "throw" the error safely by emitting it
            var err = new Error("Can't divide by zero")
            this.emit('error', err)
        }
        else {
            // no error occured, continue on
            this.emit('divided', x, y, x/y)
        }
    
        // Chain
        return this;
    }
    
    // Create our divider and listen for errors
    var divider = new Divider()
    divider.on('error', function(err){
        // handle the error safely
        console.log(err)
    })
    divider.on('divided', function(x,y,result){
        console.log(x+'/'+y+'='+result)
    })
    
    // Divide
    divider.divide(4,2).divide(4,0)

"Attraper" les erreurs en toute sécurité

Parfois, cependant, il peut toujours y avoir du code qui génère une erreur quelque part, ce qui peut entraîner une exception non interceptée et un crash potentiel de notre application si nous ne l'attrapons pas en toute sécurité. Selon notre architecture de code, nous pouvons utiliser l'une des méthodes suivantes pour l'attraper:

  • Lorsque nous savons où l'erreur se produit, nous pouvons envelopper cette section dans un domaine node.js

    var d = require('domain').create()
    d.on('error', function(err){
        // handle the error safely
        console.log(err)
    })
    
    // catch the uncaught errors in this asynchronous or synchronous code block
    d.run(function(){
        // the asynchronous or synchronous code that we want to catch thrown errors on
        var err = new Error('example')
        throw err
    })
  • Si nous savons où l'erreur se produit, c'est du code synchrone, et pour une raison quelconque, nous ne pouvons pas utiliser de domaines (peut-être l'ancienne version du nœud), nous pouvons utiliser l'instruction try catch:

    // catch the uncaught errors in this synchronous code block
    // try catch statements only work on synchronous code
    try {
        // the synchronous code that we want to catch thrown errors on
        var err = new Error('example')
        throw err
    } catch (err) {
        // handle the error safely
        console.log(err)
    }

    Cependant, veillez à ne pas utiliser try...catchdans le code asynchrone, car une erreur levée de manière asynchrone ne sera pas détectée:

    try {
        setTimeout(function(){
            var err = new Error('example')
            throw err
        }, 1000)
    }
    catch (err) {
        // Example error won't be caught here... crashing our app
        // hence the need for domains
    }

    Si vous souhaitez travailler avec try..catchen conjonction avec du code asynchrone, lorsque vous exécutez Node 7.4 ou supérieur, vous pouvez utiliserasync/await nativement pour écrire vos fonctions asynchrones.

    Une autre chose à laquelle vous devez faire attention try...catchest le risque d'encapsuler votre rappel de fin dans l' tryinstruction comme ceci:

    var divide = function(x,y,next) {
        // if error condition?
        if ( y === 0 ) {
            // "throw" the error safely by calling the completion callback
            // with the first argument being the error
            next(new Error("Can't divide by zero"))
        }
        else {
            // no error occured, continue on
            next(null, x/y)
        }
    }
    
    var continueElsewhere = function(err, result){
            throw new Error('elsewhere has failed')
    }
    
    try {
            divide(4, 2, continueElsewhere)
            // ^ the execution of divide, and the execution of 
            //   continueElsewhere will be inside the try statement
    }
    catch (err) {
            console.log(err.stack)
            // ^ will output the "unexpected" result of: elsewhere has failed
    }

    Ce gotcha est très facile à faire car votre code devient plus complexe. En tant que tel, il est préférable d'utiliser des domaines ou de renvoyer des erreurs pour éviter (1) les exceptions non interceptées dans le code asynchrone (2) l'exécution de capture catch try que vous ne voulez pas. Dans les langages qui permettent un threading approprié au lieu du style de machine d'événement asynchrone de JavaScript, cela pose moins de problème.

  • Enfin, dans le cas où une erreur non interceptée se produit dans un endroit qui n'était pas encapsulé dans un domaine ou une instruction try catch, nous pouvons empêcher notre application de se bloquer en utilisant l' uncaughtExceptionécouteur (cependant, cela peut mettre l'application dans un état inconnu ):

    // catch the uncaught errors that weren't wrapped in a domain or try catch statement
    // do not use this in modules, but only in applications, as otherwise we could have multiple of these bound
    process.on('uncaughtException', function(err) {
        // handle the error safely
        console.log(err)
    })
    
    // the asynchronous or synchronous code that emits the otherwise uncaught error
    var err = new Error('example')
    throw err
balupton
la source
5
Merci Raynos, mis à jour. Avez-vous une source qui explique les maux de try catch? Comme j'aimerais étayer cela avec des preuves. Correction également de l'exemple de synchronisation.
balupton
2
Cette réponse n'est plus valable. Domains résout ce problème (recommandé par node.js)
Gabriel Llamas
5
@balupton Des erreurs doivent être lancées pour la gestion des erreurs. Ils ne doivent absolument PAS être évités. Il n'y a rien en eux qui interrompt l'exécution de l'application ou quoi que ce soit d'autre. Java et la plupart des autres langages modernes ont un excellent support pour les exceptions. Ma seule conclusion après avoir lu certains des messages mal informés ici est que les gens ne les comprennent pas très bien et ont donc peur d'eux. Craignez le doute incertain. Ce débat a été décidé de manière concluante en faveur des exceptions il y a au moins 20 ans.
enl8enmentnow
22
Désormais, les domaines sont dépréciés par io.js : " Ce module est en attente de dépréciation. Une fois qu'une API de remplacement aura été finalisée, ce module sera complètement déprécié ... Les utilisateurs qui doivent absolument disposer des fonctionnalités que les domaines fournissent peuvent en dépendre pour le moment mais devrait s'attendre à devoir migrer vers une autre solution à l'avenir. "
Timothy Gu
5
L' API de domaine est maintenant obsolète ? Ils mentionnent une API de remplacement - quelqu'un sait quand cela sortira et à quoi cela ressemblera?
UpTheCreek
95

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

Yonatan
la source
30

Vous pouvez intercepter des exceptions non capturées, mais son utilisation est limitée. Voir http://debuggable.com/posts/node-js-dealing-with-uncaught-exceptions:4c933d54-1428-443c-928d-4e1ecbdd56cb

monit, foreverou upstartpeut être utilisé pour redémarrer le processus de nœud lorsqu'il se bloque. Vous pouvez espérer un arrêt gracieux (par exemple, enregistrer toutes les données en mémoire dans un gestionnaire d'exceptions non capturé).

nponeccop
la source
4
+1 Le lien est utile, merci. Je suis toujours à la recherche des meilleures pratiques et de la signification de "redémarrage gracieux" dans le contexte de node.js
momo
Ma compréhension du "redémarrage gracieux" dans ce contexte serait essentiellement ce que nponeccop suggère: laissez le processus mourir, et laissez tout ce qui l'exécute en premier lieu le redémarrer.
Ilkka
Merci beaucoup pour ce lien! Vraiment utile!
SatheeshJM
C'est une excellente réponse. Cependant, je ne serais pas d'accord pour renvoyer une erreur dans votre premier exemple. Renvoyer un Errorrend la valeur de retour polymorphe qui brouille inutilement la sémantique de la fonction. De plus, la plongée par 0 est déjà traitée en JavaScript en donnant Infinity, -Infinityou NaN, les valeurs où typeof === 'number'. Ils peuvent être vérifiés avec !isFinite(value). En général, je recommanderais de ne jamais renvoyer une erreur d'une fonction. Mieux en termes de lisibilité et de maintenance du code pour lancer ou renvoyer une valeur spéciale non polymorphe avec une sémantique cohérente.
wprl
13

Les domaines nodejs sont le moyen le plus à jour de gérer les erreurs dans nodejs. Les domaines peuvent capturer à la fois des événements d'erreur / autres ainsi que des objets traditionnellement levés. Les domaines fournissent également des fonctionnalités pour gérer les rappels avec une erreur transmise comme premier argument via la méthode d'interception.

Comme avec la gestion normale des erreurs de style try / catch, il est généralement préférable de lever les erreurs lorsqu'elles se produisent et de bloquer les zones où vous souhaitez isoler les erreurs d'affecter le reste du code. Le moyen de "bloquer" ces zones consiste à appeler domain.run avec une fonction de bloc de code isolé.

En code synchrone, ce qui précède est suffisant - lorsqu'une erreur se produit, vous pouvez soit la laisser passer, soit l'attraper et la gérer, en rétablissant toutes les données dont vous avez besoin.

try {  
  //something
} catch(e) {
  // handle data reversion
  // probably log too
}

Lorsque l'erreur se produit dans un rappel asynchrone, vous devez soit être en mesure de gérer entièrement la restauration des données (état partagé, données externes comme les bases de données, etc.). OU vous devez définir quelque chose pour indiquer qu'une exception s'est produite - chaque fois que vous vous souciez de cet indicateur, vous devez attendre la fin du rappel.

var err = null;
var d = require('domain').create();
d.on('error', function(e) {
  err = e;
  // any additional error handling
}
d.run(function() { Fiber(function() {
  // do stuff
  var future = somethingAsynchronous();
  // more stuff

  future.wait(); // here we care about the error
  if(err != null) {
    // handle data reversion
    // probably log too
  }

})});

Une partie de ce code ci-dessus est moche, mais vous pouvez créer des modèles pour le rendre plus joli, par exemple:

var specialDomain = specialDomain(function() {
  // do stuff
  var future = somethingAsynchronous();
  // more stuff

  future.wait(); // here we care about the error
  if(specialDomain.error()) {
    // handle data reversion
    // probably log too
  } 
}, function() { // "catch"
  // any additional error handling
});

MISE À JOUR (2013-09):

Ci-dessus, j'utilise un futur qui implique la sémantique des fibres , qui vous permet d'attendre les futures en ligne. Cela vous permet en fait d'utiliser des blocs try-catch traditionnels pour tout - ce que je trouve être la meilleure façon de procéder. Cependant, vous ne pouvez pas toujours le faire (c'est-à-dire dans le navigateur) ...

Il existe également des futurs qui ne nécessitent pas de sémantique de fibres (qui fonctionnent ensuite avec du JavaScript normal pour navigateur). Ceux-ci peuvent être appelés contrats à terme, promesses ou différés (je ferai référence aux contrats à terme à partir d'ici). Les bibliothèques de futures JavaScript simples permettent de propager les erreurs entre les futures. Seules certaines de ces bibliothèques permettent de gérer correctement tout avenir jeté, alors méfiez-vous.

Un exemple:

returnsAFuture().then(function() {
  console.log('1')
  return doSomething() // also returns a future

}).then(function() {
  console.log('2')
  throw Error("oops an error was thrown")

}).then(function() {
  console.log('3')

}).catch(function(exception) {
  console.log('handler')
  // handle the exception
}).done()

Cela imite un try-catch normal, même si les pièces sont asynchrones. Il imprimerait:

1
2
handler

Notez qu'il n'imprime pas «3» car une exception a été levée qui interrompt ce flux.

Jetez un œil aux promesses de Bluebird:

Notez que je n'ai pas trouvé beaucoup d'autres bibliothèques autres que celles-ci qui gèrent correctement les exceptions levées. Le différé de jQuery, par exemple, ne le fait pas - le gestionnaire «fail» ne verrait jamais l'exception levée par un gestionnaire «then», qui à mon avis est un facteur de rupture.

BT
la source
La spécification de promesses appropriée en Javascript est connue sous le nom de Promises / A +. Vous pouvez voir une liste d'implémentations ici: github.com/promises-aplus/promises-spec/blob/master/… . Notez qu'un Promises / A + nu est inutilisable dans la pratique - Promises / A + laisse encore beaucoup de problèmes pratiques à résoudre par les bibliothèques. Cependant, des éléments absolument essentiels comme la propagation des erreurs que vous montrez, l'ordre d'exécution déterministe et la sécurité contre le débordement de la pile sont garantis.
Esailija
11

J'ai récemment écrit à ce sujet sur http://snmaynard.com/2012/12/21/node-error-handling/ . Une nouvelle fonctionnalité du nœud dans la version 0.8 est les domaines et vous permet de combiner toutes les formes de gestion des erreurs en un seul formulaire de gestion plus facile. Vous pouvez en lire plus dans mon article.

Vous pouvez également utiliser quelque chose comme Bugsnag pour suivre vos exceptions non capturées et être averti par e-mail, sur le chat ou faire créer un ticket pour une exception non capturée (je suis le co-fondateur de Bugsnag).

Simon Maynard
la source
2
Le module de domaine est désormais officiellement obsolète. nodejs.org/api/domain.html
MattSidor
3

Je voudrais juste ajouter que la bibliothèque Step.js vous aide à gérer les exceptions en la passant toujours à la fonction de l'étape suivante. Par conséquent, vous pouvez avoir comme dernière étape une fonction qui vérifie les erreurs dans l'une des étapes précédentes. Cette approche peut simplifier considérablement la gestion des erreurs.

Voici une citation de la page github:

toutes les exceptions levées sont interceptées et passées comme premier argument à la fonction suivante. Tant que vous n'imbriquez pas de fonctions de rappel dans vos fonctions principales, cela empêche qu'il n'y ait jamais d'exceptions non capturées. Ceci est très important pour les serveurs node.JS de longue durée, car une seule exception non interceptée peut entraîner l'arrêt complet du serveur.

De plus, vous pouvez utiliser Step pour contrôler l'exécution des scripts afin d'avoir une section de nettoyage comme dernière étape. Par exemple, si vous souhaitez écrire un script de génération dans Node et indiquer le temps qu'il a fallu pour écrire, la dernière étape peut le faire (plutôt que d'essayer de déterrer le dernier rappel).

Michael Yagudaev
la source
3

Une instance où l'utilisation d'un try-catch peut être appropriée est lors de l'utilisation d'une boucle forEach. Il est synchrone mais en même temps, vous ne pouvez pas simplement utiliser une instruction de retour dans la portée interne. Au lieu de cela, une approche try and catch peut être utilisée pour renvoyer un objet Error dans la portée appropriée. Considérer:

function processArray() {
    try { 
       [1, 2, 3].forEach(function() { throw new Error('exception'); }); 
    } catch (e) { 
       return e; 
    }
}

Il s'agit d'une combinaison des approches décrites par @balupton ci-dessus.

Michael Yagudaev
la source
Au lieu de lancer des erreurs, certains développeurs recommandent d'utiliser le concept de résultat de Rust pour renvoyer un OK ou un échec , lorsque l'échec est une possibilité connue. Cela empêche les échecs des erreurs inattendues. Une implémentation JS de ceci est r-result .
joeytwiddle
C'est une décision de conception à l'échelle de l'application. Je pense que votre concept de retour d'erreurs est à peu près équivalent et simple à démarrer (pas de dépendances supplémentaires), mais moins explicite (le résultat vous rend douloureusement conscient lorsque des échecs doivent être traités) et moins efficace dans les cas où une pile est construit inutilement.
joeytwiddle
1

Après avoir lu ce post il y a quelque temps, je me demandais s'il était sûr d'utiliser des domaines pour la gestion des exceptions au niveau de l'api / fonction. Je voulais les utiliser pour simplifier le code de gestion des exceptions dans chaque fonction asynchrone que j'ai écrite. Ma préoccupation était que l'utilisation d'un nouveau domaine pour chaque fonction entraînerait une surcharge importante. Mes devoirs semblent indiquer que les frais généraux sont minimes et que les performances sont en fait meilleures avec les domaines qu'avec try catch dans certaines situations.

http://www.lighthouselogic.com/#/using-a-new-domain-for-each-async-function-in-node/

Sudsy
la source
1

La détection des erreurs a été très bien discutée ici, mais il convient de se rappeler de déconnecter les erreurs quelque part afin de pouvoir les afficher et corriger les problèmes.

Bunyan est un cadre de journalisation populaire pour NodeJS - il supprime l'écriture dans un tas d'endroits de sortie différents, ce qui le rend utile pour le débogage local, tant que vous évitez console.log. Dans le gestionnaire d'erreurs de votre domaine, vous pouvez cracher l'erreur dans un fichier journal.

var log = bunyan.createLogger({
  name: 'myapp',
  streams: [
    {
      level: 'error',
      path: '/var/tmp/myapp-error.log'  // log ERROR to this file
    }
  ]
});

Cela peut prendre du temps si vous avez beaucoup d'erreurs et / ou de serveurs à vérifier, il pourrait donc être utile d'examiner un outil comme Raygun (clause de non-responsabilité, je travaille chez Raygun) pour regrouper les erreurs - ou les utiliser les deux ensemble. Si vous avez décidé d'utiliser Raygun comme outil, il est également assez facile à configurer

var raygunClient = new raygun.Client().init({ apiKey: 'your API key' });
raygunClient.send(theError);

Croisée avec l'utilisation d'un outil comme PM2 ou pour toujours, votre application devrait pouvoir planter, déconnecter ce qui s'est passé et redémarrer sans aucun problème majeur.

K. Craven
la source