Introduction
Les erreurs de Node.js en production se manifestent rarement avec une cause racine claire. Elles se présentent sous forme de trace de pile dans un agrégateur de journaux, d'un délai d'attente de vérification de santé, d'un pic de réponses 502 provenant d'un proxy inverse, ou d'un développeur signalant qu'un service fonctionne sur son ordinateur portable mais pas dans l'environnement de préproduction. Un opérateur fiable doit passer du symptôme à la correction vérifiée sans provoquer un second incident, ce qui implique de savoir quoi observer en premier, quel changement est le plus limité et comment prouver que le problème est résolu.
Ce guide est destiné aux développeurs, ingénieurs DevOps et équipes techniques de startups qui exploitent des services Node.js et souhaitent une approche reproductible du dépannage. Il montre comment gérer les messages d'erreur courants de Node.js, comment déboguer sans disséminer des instructions de journalisation, et comment résoudre les problèmes avec des commandes adaptées à la version. Tout au long, il utilise des exemples pratiques avec des espaces réservés explicites et des sorties attendues, afin que vous puissiez les adapter à votre environnement sans divulguer de secrets ni deviner le résultat.
L'article suppose un déploiement Node.js typique : une application s'exécutant derrière un gestionnaire de processus tel que systemd ou PM2, éventuellement derrière un proxy inverse comme Nginx, et utilisant souvent des services externes comme MongoDB ou Redis. Les mêmes principes s'appliquent aux flux de travail locaux plus petits, mais les exemples ciblent un service qu'un opérateur peut observer et redémarrer.
Une règle opérationnelle traverse chaque section : observer avant de modifier. Capturez l'état actuel et les horodatages, définissez le résultat attendu et le signal d'échec, effectuez le changement le plus limité possible, vérifiez le résultat et sachez comment récupérer si l'état attendu n'est pas atteint. Sauter l'une de ces étapes transforme une session de débogage en conjectures et peut aggraver l'erreur d'origine.
Inventaire des versions et de l'environnement
Avant de toucher à un processus Node.js en cours d'exécution, établissez ce avec quoi vous travaillez. Une incompatibilité de version ou un prérequis manquant peut expliquer de nombreuses erreurs courantes avant même d'inspecter le code de l'application. Les commandes en lecture seule suivantes vous donnent un inventaire complet en moins d'une minute.
Tout d'abord, vérifiez la version installée de Node.js et la version de npm :
node -v
npm -v
La sortie attendue sur une version LTS actuelle ressemble à ceci :
v20.11.1
10.2.4
Si la sortie affiche une version non prise en charge (par exemple, v12) et que votre application utilise des fonctionnalités d'un runtime plus récent, la première correction consiste à mettre à niveau Node.js vers une ligne LTS prise en charge, et non à ajouter des polyfills ou à réécrire le code. Enregistrez la sortie de node -v avant la mise à niveau afin de pouvoir revenir en arrière si nécessaire.
Ensuite, identifiez la topologie de déploiement. Quel gestionnaire de processus exécute le service ? Pour systemd, exécutez :
systemctl status my-node-app --no-pager
La commande affiche l'état du service, les lignes de journal récentes et l'identifiant du processus. Recherchez des lignes comme :
Active: active (running) since Tue 2025-01-14 08:12:33 UTC; 1h 26min ago
Si le service n'est pas actif ou est dans une boucle de redémarrage, la sortie affichera failed ou activating (auto-restart) avec des codes de sortie récents. C'est un signal concret pour inspecter les journaux avant d'apporter des modifications.
Pour les utilisateurs de PM2, inventorier la liste des processus avec :
pm2 list
Un processus sain affiche online, un nombre de redémarrages de 0 ou un nombre faible, et une durée de fonctionnement raisonnable. Si le nombre de redémarrages augmente toutes les quelques secondes, le processus est en boucle de plantage et vous devez vérifier les journaux avec pm2 logs my-node-app --lines 50.
Les prérequis sont importants. De nombreuses erreurs de Node.js proviennent de modules natifs compilés contre une version différente de Node ou d'une bibliothèque système manquante. Vérifiez l'environnement de construction avec :
node -p "process.versions"
Cela imprime un objet avec les versions de Node, V8, OpenSSL et autres. Comparez la sortie avec la version avec laquelle vos dépendances natives ont été construites. Par exemple, si bcrypt a été installé sur Node 18 et que vous passez à Node 20, vous pouvez voir une erreur comme Error: The module was compiled against a different Node.js version. La correction n'est pas de rétrograder Node ; au lieu de cela, reconstruisez les modules natifs avec npm rebuild bcrypt après la mise à niveau.
Lors de la vérification des dépendances externes, ne divulguez jamais d'informations d'identification. Une façon sûre de tester la connectivité à MongoDB est d'utiliser le shell MongoDB avec une chaîne de connexion d'espace réservé :
mongosh "mongodb://USERNAME:PASSWORD@HOST:27017/DATABASE" --eval "db.runCommand({ ping: 1 })"
Remplacez les valeurs en majuscules par vos variables d'environnement réelles. Un ping réussi renvoie { ok: 1 }. Pour Redis, utilisez :
redis-cli -h HOST -p PORT -a PASSWORD ping
La sortie attendue est PONG. Si vous voyez NOAUTH Authentication required, les informations d'identification ou la configuration sont incorrectes, pas la logique de l'application.
Enregistrez ces observations dans un journal d'incident avant de modifier quoi que ce soit. Les horodatages sont essentiels : un service dégradé peut avoir démarré à un moment précis, et la corrélation des horodatages entre les journaux peut révéler le déclencheur.
Chemin de configuration sûr
Les erreurs de configuration sont parmi les problèmes Node.js les plus courants car la configuration est souvent dispersée entre les variables d'environnement, les fichiers JSON et les valeurs par défaut en ligne. Une seule faute de frappe dans un nom de variable peut casser un service entier au démarrage ou, pire, le faire fonctionner avec des valeurs par défaut non sécurisées.
L'approche la plus sûre est de traiter la configuration comme du code : la garder versionnée, appliquer les modifications de manière contrôlée et vérifier avec une commande cohérente. Commencez par lister les variables d'environnement actives que le processus voit. Sur un service géré par systemd, la configuration est dans le fichier d'unité :
systemctl cat my-node-app
Cela affiche le fichier d'unité complet, y compris les lignes Environment=. Confirmez que les noms de variables correspondent à ce que l'application attend. Une erreur courante est d'appeler la variable DB_HOST dans la configuration alors que l'application lit DATABASE_HOST. Le signal d'échec est souvent une erreur ECONNREFUSED ou une ligne de journal indiquant DATABASE_HOST is not defined.
Pour les applications Node.js qui utilisent un fichier .env, inspectez les valeurs analysées sans imprimer les secrets. Un petit script peut le faire en toute sécurité :
const env = require('dotenv').config().parsed;
console.log(Object.keys(env).filter(k => k.includes('DB_') || k.includes('REDIS_')).map(k => `${k}=<set>`).join('\n'));
Exécutez ceci avec node check-env.js. Cela n'imprime que les noms de clés, pas les valeurs, confirmant que les clés attendues existent. Si une clé attendue est manquante, la sortie ne l'inclura pas. Cela empêche la divulgation accidentelle de secrets tout en validant la forme de la configuration.
Le rayon d'impact d'un changement de configuration dépend de l'endroit où le changement est effectué. Modifier une unité systemd nécessite un redémarrage, ce qui interrompt brièvement le service. Modifier un fichier .env nécessite le prochain démarrage du processus pour le prendre en compte, donc si le service fork des processus enfants, le changement peut ne pas s'appliquer aux travailleurs déjà en cours d'exécution. Une alternative plus sûre est d'utiliser un outil de gestion de configuration comme consul-template ou envsubst avec un fichier de configuration modélisé, mais le principe reste : effectuez un changement limité, puis vérifiez.
La vérification implique toujours de vérifier le comportement du service, pas seulement son état de démarrage. Par exemple, si vous modifiez la chaîne de connexion Redis, démarrez le service et testez le point de terminaison dépendant de Redis :
curl -s http://localhost:3000/health/redis
Sortie attendue avec une connexion saine :
{"status":"ok","redis":"connected"}
Si la sortie indique "redis":"disconnected", annulez le changement de configuration et inspectez les journaux du serveur Redis. Le chemin de récupération doit être documenté dans votre manuel d'exploitation : conservez le fichier de configuration précédent avec un horodatage et restaurez-le avec cp config.previous config suivi d'un rechargement du service.
Un piège de configuration subtil est la variable NODE_ENV. Définir NODE_ENV=production modifie le comportement des dépendances (par exemple, Express masque les traces de pile), mais certaines bibliothèques se comportent différemment lorsque NODE_ENV n'est pas défini ou est défini sur test. Un NODE_ENV mal configuré peut amener la production à fonctionner avec une sortie d'erreur verbeuse ou le développement à supprimer les informations de débogage. Vérifiez toujours que NODE_ENV correspond au contexte de déploiement :
systemctl show my-node-app -p Environment | tr ' ' '\n' | grep NODE_ENV
Sortie attendue sur un hôte de production : NODE_ENV=production. Si elle est vide ou définie sur development, mettez à jour le fichier d'unité et rechargez systemd.
Vérification et diagnostics
La vérification est l'étape qui sépare une conjecture d'une correction. Un modèle d'échec courant consiste à changer la première chose qui semble incorrecte, redémarrer le service et déclarer victoire lorsque le processus démarre. Cela ne prouve pas que l'erreur d'origine a disparu ; cela prouve seulement que le service peut démarrer. La bonne approche consiste à définir un signal spécifique et observable qui confirme la correction.
Commencez par les signaux de diagnostic intégrés que Node.js expose. Pour un service en cours d'exécution, vérifiez sa mémoire et son comportement de boucle d'événements sans l'arrêter. Vous pouvez utiliser l'API process._getActiveHandles() dans un script ponctuel qui se connecte aux mêmes dépendances que le service utilise. Plus pratiquement, ajoutez un point de terminaison qui rapporte la santé de base :
app.get('/health', (req, res) => {
res.json({
uptime: process.uptime(),
memory: process.memoryUsage(),
version: process.version
});
});
Ce point de terminaison vous donne une base de comparaison. Avant une correction, l'utilisation de la mémoire peut être de 200 Mo et en croissance. Après la correction, elle doit être stable ou plus faible sur une fenêtre de temps similaire. Un instantané unique ne suffit pas ; tracez la métrique sur une heure.
Pour de nombreuses erreurs courantes de Node.js, le message d'erreur lui-même est le diagnostic. Apprenez à interpréter le code d'erreur. Par exemple, ECONNRESET dans une ligne de journal signifie généralement qu'un pair a fermé la connexion de manière inattendue, souvent parce qu'un service en amont a terminé la requête. EADDRINUSE indique un conflit de port. ENOSPC signifie que le disque est plein. Avant de corriger, reproduisez l'erreur avec une commande contrôlée.
Pour diagnostiquer les conflits de port, exécutez :
lsof -i :3000
La sortie attendue montre l'identifiant du processus et la commande détenant le port 3000. Si vous voyez un processus obsolète d'un déploiement précédent, terminez-le avec kill PID (où PID est l'identifiant réel du processus), puis démarrez le service. Cette vérification prouve que le port est libre avant de tenter de se lier.
Pour les exceptions non capturées, ajoutez des gestionnaires au niveau du processus qui journalisent l'erreur sans faire planter le service jusqu'à ce que vous puissiez enquêter. En production, c'est un palliatif, pas une correction :
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled rejection at', promise, 'reason:', reason);
});
Avec ce gestionnaire, l'erreur est visible dans les journaux mais le processus continue. La correction réelle est de gérer le rejet dans le chemin du code, mais le gestionnaire vous donne le temps de le reproduire. Une meilleure approche à long terme est d'utiliser un outil de diagnostic comme node --inspect dans un environnement de préproduction.
Connectez l'inspecteur à un processus en cours d'exécution avec :
node --inspect=0.0.0.0:9229 app.js
Ensuite, ouvrez Chrome DevTools à chrome://inspect. Cela expose les profils CPU, les instantanés de tas et les points d'arrêt. Si le service fuit de la mémoire, prenez deux instantanés de tas à dix minutes d'intervalle et comparez la taille retenue. Les objets avec la plus grande croissance sont probablement la fuite.
La vérification doit également inclure l'absence de l'erreur d'origine. Si l'erreur était un plantage à chaque requête vers une route spécifique, exécutez une boucle de requêtes vers cette route et confirmez zéro échec :
for i in {1..100}; do curl -s -o /dev/null -w "%{http_code}\n" http://localhost:3000/critical-route; done | sort | uniq -c
Sortie attendue après la correction :
100 200
Si vous voyez des 500, la correction n'a pas adressé la cause racine et vous avez besoin de plus de diagnostics.
Modes d'échec et récupération
Chaque service Node.js a quelques modes d'échec pour lesquels il vaut la peine de se préparer. Un mode d'échec n'est pas la même chose qu'un message d'erreur ; c'est toute une classe d'incident avec un déclencheur connu, un impact et un chemin de récupération. Documenter ceux-ci avant qu'ils ne surviennent réduit le temps moyen de récupération.
Un mode d'échec courant est une fuite de mémoire qui fait que le processus épuise le tas et plante avec FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory. La récupération immédiate est de redémarrer le processus :
systemctl restart my-node-app
Cela restaure le service mais ne corrige pas la fuite. La correction appropriée nécessite un profilage avec des instantanés de tas comme décrit dans la section précédente. En attendant, vous pouvez augmenter temporairement la limite du tas :
node --max-old-space-size=4096 app.js
Cela donne au processus plus de mémoire mais ne fait que retarder l'inévitable. La correction à long terme adaptée à la version est d'identifier les objets qui fuient et de supprimer les références. Ce mode d'échec a un impact élevé car un plantage interrompt toutes les requêtes, alors configurez une alerte sur la croissance du tas avant qu'elle n'atteigne 90 % de la limite.
Un autre mode d'échec est la perte de connectivité à une base de données comme MongoDB ou Redis. Le symptôme est un pic d'erreurs ECONNREFUSED ou ETIMEDOUT. La récupération signifie souvent basculer vers un réplica ou redémarrer la base de données, mais le service Node.js doit gérer la reconnexion avec élégance. De nombreux pilotes le font automatiquement, mais une nouvelle tentative au niveau de l'application avec un backoff exponentiel est plus sûre :
async function connectWithRetry() {
for (let attempt = 1; attempt <= 5; attempt++) {
try {
await mongoose.connect(process.env.MONGO_URI);
console.log('MongoDB connected');
return;
} catch (err) {
console.error(`Attempt ${attempt} failed: ${err.message}`);
await new Promise(resolve => setTimeout(resolve, 1000 * 2 ** attempt));
}
}
process.exit(1);
}
Cela réessaie cinq fois avec des délais croissants. Si toutes les tentatives échouent, le processus se termine pour que le gestionnaire de processus puisse le redémarrer proprement. Sans ce modèle, un service peut fonctionner indéfiniment avec une connexion de base de données morte, renvoyant des 500 à chaque requête.
Un troisième mode d'échec est une boucle de plantage causée par une erreur de syntaxe introduite lors d'un déploiement. Le service démarre, ne parvient pas à analyser un fichier, se termine, et le gestionnaire de processus le redémarre, répétant indéfiniment. Le signal est une augmentation rapide du nombre de redémarrages. Récupérez en revenant à la version de code précédente. Gardez vos artefacts de déploiement versionnés et stockez le chemin de l'artefact précédent dans une variable :
npm install
pm2 start app.js --name my-node-app
Si la nouvelle version plante, revenez en arrière avec :
cp /opt/app/releases/previous/app.js /opt/app/current/app.js
pm2 restart my-node-app
La commande de restauration doit être testée avant d'être nécessaire. Exécutez la restauration dans un environnement de préproduction et chronométrez-la ; l'objectif de temps de récupération pour une restauration doit être inférieur à cinq minutes pour un pipeline de déploiement bien conçu.
Pour chaque mode d'échec, nommez un propriétaire. Dans une petite équipe, c'est généralement l'ingénieur d'astreinte, mais ce doit être une seule personne, pas un groupe. Le propriétaire est responsable de décider quand tenter la récupération, d'exécuter les étapes documentées et de rédiger une revue post-incident. Révisez la documentation du mode d'échec trimestriellement ou après chaque incident qui révèle une lacune.
Liste de contrôle des opérations
Utilisez cette liste de contrôle avant et après toute session de dépannage Node.js. Elle consolide les sections précédentes en étapes concrètes, avec un propriétaire illustratif pour chaque élément.
| Étape | Action | Commande ou signal | Résultat attendu | Propriétaire |
|---|---|---|---|---|
| 1 | Enregistrer les versions de base | node -v et npm -v | Node v20.11.1, npm 10.2.4 | Priya Shah, responsable ingénierie |
| 2 | Vérifier l'état du processus | systemctl status my-node-app ou pm2 list | actif (en cours d'exécution), nombre de redémarrages 0 | Marcus Lee, ingénieur DevOps |
| 3 | Capturer les journaux récents | journalctl -u my-node-app --since "1 hour ago" --no-pager | aucune ECONNREFUSED ou rejet non géré au cours de la dernière heure | Elena Rodriguez, développeuse backend |
| 4 | Vérifier les clés de configuration | exécuter le script de vérification des clés uniquement | toutes les clés attendues présentes, aucun secret imprimé | Priya Shah |
| 5 | Tester la connectivité externe | ping MongoDB et Redis avec des informations d'identification d'espace réservé | MongoDB { ok: 1 }, Redis PONG | Marcus Lee |
| 6 | Effectuer une vérification de santé | curl -s http://localhost:3000/health | HTTP 200 avec les champs JSON attendus | Elena Rodriguez |
| 7 | Reproduire l'erreur | exécuter une boucle curl ciblée ou un test unitaire | code d'erreur cohérent ou zéro erreur après correction | Elena Rodriguez |
| 8 | Documenter le changement | mettre à jour le manuel d'exploitation avec horodatages, commandes et restauration | diff du manuel examiné par une deuxième personne | Priya Shah |
Chaque élément de la liste de contrôle a un propriétaire nommé, pas un groupe. Le propriétaire exécute l'étape, capture la sortie et signe dans le journal d'incident. Révisez cette liste de contrôle chaque semaine lors d'une mêlée d'équipe jusqu'à ce qu'elle devienne routinière, puis mensuellement dans le cadre d'une revue de fiabilité.
Un signal d'échec sur n'importe quelle étape signifie que vous devez vous arrêter et enquêter sur cet élément avant de continuer. Parcourir la liste de contrôle après une correction est tout aussi important que de la parcourir avant : le passage après correction confirme que rien d'autre n'a régressé.
Pièges courants et comment les éviter
Même les développeurs Node.js expérimentés commettent des erreurs prévisibles lors du débogage. Reconnaître ces pièges avant qu'ils ne surviennent permet de gagner du temps et d'éviter des incidents secondaires.
Piège 1 : Modifier le code sans reproduire l'erreur. Pourquoi cela arrive : sous pression, une correction est appliquée à la ligne de code la plus suspecte sur la seule base d'une trace de pile. Le problème est que la trace de pile peut pointer vers un symptôme, pas une cause. Comment l'éviter : reproduisez toujours l'erreur avec un cas de test minimal ou une requête répétée avant de modifier le code. Par exemple, si une route lève TypeError: Cannot read properties of undefined, écrivez un test unitaire qui frappe la route avec la même entrée provenant des journaux. Ensuite seulement, modifiez le code. Si vous ne pouvez pas la reproduire, ajoutez plus de journalisation pour capturer l'entrée qui déclenche l'échec.
Piège 2 : Redémarrer un service sans vérifier la sortie des journaux. Pourquoi cela arrive : un redémarrage est le moyen le plus rapide d'arrêter un incident, et les journaux semblent longs. Mais un redémarrage efface souvent un état critique en mémoire qui explique l'erreur. Comment l'éviter : avant de redémarrer, capturez les 100 dernières lignes de journaux avec journalctl -u my-node-app -n 100 --no-pager et enregistrez-les dans un fichier. Ensuite, redémarrez. Le fichier journal devient la preuve pour l'analyse post-incident.
Piège 3 : Utiliser console.log pour tout le débogage. Pourquoi cela arrive : c'est facile et ne nécessite pas d'outils spéciaux. Mais les instructions de journalisation modifient le timing et peuvent masquer des conditions de course ; elles ajoutent également du bruit aux journaux de production. Comment l'éviter : utilisez une journalisation structurée avec une bibliothèque comme pino dès le départ. En production, journalisez au niveau info ; ajoutez la sortie debug uniquement en développement. Utilisez l'inspecteur pour le débogage interactif et fiez-vous aux métriques pour la santé continue.
Piège 4 : Ne pas gérer les rejets de promesses globalement. Pourquoi cela arrive : dans du code plus ancien, une promesse rejetée en dehors d'une fonction asynchrone peut faire planter le processus avec une erreur unhandledRejection. Les développeurs supposent souvent que toutes les promesses sont capturées. Comment l'éviter : ajoutez un gestionnaire global comme indiqué précédemment, mais traitez-le comme un filet de sécurité, pas une correction. Configurez votre exécuteur de tests pour échouer sur les rejets non gérés et appliquez une règle de lint qui signale les promesses flottantes.
Piège 5 : Déployer des changements de configuration sans contrôle de version. Pourquoi cela arrive : la configuration vit dans des variables d'environnement ou des fichiers qui ne font pas partie du dépôt de code. Une modification rapide d'un fichier .env fonctionne pendant une journée, puis la valeur est perdue lorsque le serveur est reconstruit. Comment l'éviter : stockez les modèles de configuration dans le contrôle de version, utilisez un gestionnaire de secrets pour les valeurs réelles et exigez une demande d'extraction pour tout changement de configuration de production. Le chemin de restauration est alors un git revert au lieu d'une recherche frénétique.
Chaque piège suit le même modèle : la cause racine est un raccourci qui ignore l'observation ou la vérification. Récupérer d'un piège signifie généralement restaurer l'état précédent, capturer l'information manquante et répéter la correction avec la discipline appropriée.
Conclusion
Les erreurs courantes de Node.js deviennent gérables lorsque vous cessez de traiter chaque incident comme un puzzle unique et commencez à le traiter comme un flux de travail reproductible. Le cœur de ce flux de travail est simple : inventorier l'environnement, observer l'état actuel avec des commandes en lecture seule, définir le résultat attendu et le signal d'échec, effectuer un changement limité, vérifier avec un signal concret et savoir comment revenir en arrière si la vérification échoue.
Cet article a parcouru ce flux de travail avec des exemples pratiques : vérifier les versions de Node et npm avec node -v, inspecter l'état de systemd et PM2, valider les clés de configuration sans divulguer de secrets, utiliser des points de terminaison de santé et l'inspecteur pour les diagnostics, récupérer des fuites de mémoire et des boucles de plantage, et suivre une liste de contrôle avec des propriétaires nommés.
La prochaine étape consiste à choisir une vérification à faible risque de ce guide, comme l'inventaire de l'environnement ou la vérification de santé, et à l'exécuter sur l'un de vos services. Enregistrez l'état actuel, comparez le résultat avec la sortie attendue et notez les lacunes. Ensuite, choisissez une seule erreur courante de vos journaux et parcourez le cycle reproduire, corriger, vérifier. Avec le temps, ces cycles deviennent la mémoire musculaire opérationnelle de votre équipe, et le prochain incident sera un exercice de suivi d'un chemin connu plutôt qu'une bousculade.
Un flux de travail technique fiable rend les échecs visibles, protège les valeurs sensibles, limite les changements à la ressource prévue et définit la vérification de récupération avant qu'un incident ne force la décision. Construisez ce flux de travail maintenant, et vos services Node.js en seront plus robustes.