Gérer les erreurs avec Node.js

Gérer les erreurs avec Node.js

Lorsqu’une exception n’est pas gérée dans un programme Node.js, cela se termine en général par un crash du process de l’application. Il n’y a d’ailleurs pas grand chose à faire pour tenter de rattraper le coup si l’erreur remonte jusqu’à la boucle d’événement. C’est pourquoi, il est nécessaire ...

Node.js

Lorsqu’une exception n’est pas gérée dans un programme Node.js, cela se termine en général par un crash du process de l’application. Il n’y a d’ailleurs pas grand chose à faire pour tenter de rattraper le coup si l’erreur remonte jusqu’à la boucle d’événement. C’est pourquoi, il est nécessaire de traiter les erreurs avec attention.

Si votre programme génère une erreur qui remonte jusqu’à la boucle d’événement comme suit:

process.nextTick () ->
	throw new Error("Some Bad Error")

Vous aurez le droit au message d’erreur qui suit:

Express listening on port: 9000
Started in 0.073 seconds

/Users/akinsella/Workspace/Projects/gtfs-playground/build/app-test.js:30
    throw new Error("Some Bad Error");
          ^
Error: Some Bad Error
    at /Users/akinsella/Workspace/Projects/gtfs-playground/build/app-test.js:30:11
    at process._tickCallback (node.js:415:13)
    at Function.Module.runMain (module.js:499:11)
    at startup (node.js:119:16)
    at node.js:901:3

Process finished with exit code 8

L’événement ‘uncaughtException’

Node.js vous donne une chance d’intercepter les erreurs qui remontent jusqu’à la boucle d’événement grace au dispatch l’événement de type uncaughtExcpetion.

Contrairement à ce qu’on pourrait penser, l’événement n’est pas dispatché par le process Node.js pour catcher l’erreur et permettre de continuer au programme son exécution. C’est principalement pour gérer correctement la libération de resources qui auraient été ouvertes par le programmes, et éventuellement logger de façon plus précise le contexte de l’erreur (Etat de la mémoire, etc…).

Lorsqu’une erreur remonte jusqu’à la boucle d’événement, il ne faut plus considérer l’état du programme comme étant consistant. C’est pour cette raison qu’il ne faut pas tenter de catcher l’exception dans l’idée de permettre au programme de continuer à fonctionner.

Si vous souhaitez logger un message d’erreur dans le cas d’une exception remontée jusqu’à la boucle d’événement, vous pouvevz ajouter le code suivant à votre programme:

process.on 'uncaughtException', (err) ->
    console.log JSON.stringify(process.memoryUsage())
    console.error "An uncaughtException was found, the program will end. #{err}, stacktrace: #{err.stack}"
    process.exit 1

process.nextTick () ->
    throw new Error("Some Bad Error")

Ce qui donne le résultat suivant:

/Users/akinsella/.nvm/v0.10.22/bin/node app-test.js
{"rss":12312576,"heapTotal":4083456,"heapUsed":2153648}
An uncaughtException was found, the program will end. Error: Some Bad Error, stacktrace: Error: Some Bad Error
    at /Users/akinsella/Workspace/Projects/gtfs-playground/build/app-test.js:13:11
    at process._tickCallback (node.js:415:13)
    at Function.Module.runMain (module.js:499:11)
    at startup (node.js:119:16)
    at node.js:901:3

Process finished with exit code 1

Contrairement à la gestion par défaut, nous avons pu retourner un exit code spécifique. Ici le code retour: 1
Le message de log est également différent. Nous sommes donc en mesure de maitriser le log d’erreur en cas de crash.
Par ailleurs, les informations de mémoire rendues disponibles dans les logs participeront à faciliter l’analyse du crash.

Express

Si vous utilisez un framework type Express, vous serez déchargé d’une partie du travail car les erreurs qui interviennent pendant le traitement d’une requête HTTTP sont catchées par le framework qui gérera pour vous l’erreur.

Par défaut Express se contente de logger un crash qui intervient dans le traitement d’une requête HTTP via un simple log retourné dans la réponse HTTP.

Par exemple, en exécutant le programme suivant:

express = require 'express'

app = express()

app.configure ->
    app.set 'port', process.env.PORT or 9000
    app.use app.router

app.get "/", (req, res) ->
    throw new Error("Some Bad Error")

httpServer = app.listen app.get('port')

process.on 'uncaughtException', (err) ->
    console.error "An uncaughtException was found, the program will end. #{err}, stacktrace: #{err.stack}"
    process.exit 1

console.error "Express listening on port: #{app.get('port')}"

Puis en se rendant sur l’url http://localhost:9000, Express renverra dans la réponse HTTP le log suivant:

Error: Some Bad Error
    at /Users/akinsella/Workspace/Projects/gtfs-playground/build/app-test.js:16:11
    at callbacks (/Users/akinsella/Workspace/Projects/gtfs-playground/node_modules/express/lib/router/index.js:164:37)
    at param (/Users/akinsella/Workspace/Projects/gtfs-playground/node_modules/express/lib/router/index.js:138:11)
    at pass (/Users/akinsella/Workspace/Projects/gtfs-playground/node_modules/express/lib/router/index.js:145:5)
    at Router._dispatch (/Users/akinsella/Workspace/Projects/gtfs-playground/node_modules/express/lib/router/index.js:173:5)
    at Object.router (/Users/akinsella/Workspace/Projects/gtfs-playground/node_modules/express/lib/router/index.js:33:10)
    at next (/Users/akinsella/Workspace/Projects/gtfs-playground/node_modules/express/node_modules/connect/lib/proto.js:193:15)
    at Object.expressInit [as handle] (/Users/akinsella/Workspace/Projects/gtfs-playground/node_modules/express/lib/middleware.js:30:5)
    at next (/Users/akinsella/Workspace/Projects/gtfs-playground/node_modules/express/node_modules/connect/lib/proto.js:193:15)
    at Object.query [as handle] (/Users/akinsella/Workspace/Projects/gtfs-playground/node_modules/express/node_modules/connect/lib/middleware/query.js:45:5)

Il est également possible d’activer un log plus détaillé, avec une mise en forme HTML, particulièrement utile en mode développement en ajoutant les lignes suivantes:

app.configure 'development', () ->
    app.use express.errorHandler
        dumpExceptions: true,
        showStack: true

Le résultat sera le suivant:

express-error

Express vous permet également de renseigner un middleware qui aura la possibilité d’interagir les erreurs rencontrées dans le traitement des requêtes HTTP. Ce middleware peut être utile pour logger l’erreur rencontrée ou bien encore libérer des resources associées à la requête en cours de traitement.

Il permettra également de renvoyer une réponse adaptée à l’utilisateur en cas d’erreur non gérée. Ce point particulièrement intéressant dans le cas de l’implémentation d’API REST. Le serveur devient capable de renvoyer une erreur interprétable par le client même en cas d’erreur non gérée.

Le middleware prendra la format suivant:

app.use (err, req, res, next) ->
        console.error "Error: #{err}, Stacktrace: #{err.stack}"
        res.send 500, "Something broke! Error: #{err}, Stacktrace: #{err.stack}"

Les promises

Les promises peuvent vous aider à gérer les erreurs plus efficacement grâce à leur mécanisme de gestion des erreurs.

Un traitement encapsulé dans une promise ne permettra jamais à une erreur de remonter jusqu’à l’event loop, l’erreur sera catchée par la promise qui sera remontée dans la fonction fail ou catch selon la librairie ou bien encore dans le callback d’erreur de la fonction then.

Il est donc intéressant d’encapsuler vos traitement avec des promises, non seulement pour améliorer la lisibilité du code, mais également pour sa capacité à résister aux crashs.

Les domaines

La notion de domain ne sera pas traitée dans cet article, sachez néanmoins que cette notion a été ajoutée à Node.js en version 0.10.

En bref et pour faire simple, l’idée est plus ou moins de containeriser des event emitters en les associant à un domain. En cas d’erreur dans le traitement d’un événement géré par un domain, l’exception ne fera pas crasher le programme directement, c’est le domain qui sera en charge de traiter l’erreur, mais cela ne vous sauvera pas en général d’un redémarrage du process … comme en témoigne la documentation:

Domain error handlers are not a substitute for closing down your process when an error occurs.

By the very nature of how throw works in JavaScript, there is almost never any way to safely “pick up where you left off”, without leaking references, or creating some other sort of undefined brittle state.

The safest way to respond to a thrown error is to shut down the process. Of course, in a normal web server, you might have many connections open, and it is not reasonable to abruptly shut those down because an error was triggered by someone else.

The better approach is send an error response to the request that triggered the error, while letting the others finish in their normal time, and stop listening for new requests in that worker.

In this way, domain usage goes hand-in-hand with the cluster module, since the master process can fork a new worker when a worker encounters an error. For node programs that scale to multiple machines, the terminating proxy or service registry can take note of the failure, and react accordingly.

La documentation de Node.js relative aux domains est disponibles à l’url suivante: