La plupart des tutoriels Node.js traitent la gestion des erreurs comme une réflexion après coup. Vous enveloppez un gestionnaire de route dans un bloc try/catch, vous journalisez la trace de la pile et vous renvoyez une erreur 500. Cet état d'esprit perdure parce qu'une personne réelle attend à l'autre bout d'une requête HTTP. Les tâches de fond sont différentes. Dans un système de file d'attente, il n'y a pas de client impatient à qui répondre, pas de rafraîchissement automatique du navigateur. Il n'y a qu'un worker, une charge utile et un compteur de tentatives qui grimpe en silence. Quand les choses tournent mal, elles tournent mal lentement, puis d'un coup. Une erreur mal classifiée peut paralyser un pipeline entier ou faire intervenir un ingénieur à trois heures du matin.
Le décalage est simple. Les cycles requête-réponse échouent rapidement et de manière bruyante. Un échec de file d'attente est silencieux. Un worker peut enchaîner des centaines de tâches avant qu'une connexion à la base de données ne soit perdue. Sans règles de gestion claires, le worker réessaie instantanément, martèle la base de données déjà en difficulté et finit par planter. Comme personne ne surveille directement le worker, le premier signe de problème est souvent un engorgement en cascade ou un disque saturé de fichiers journaux. Vous avez besoin de plus que de simples blocs catch. Vous avez besoin d'une stratégie qui traite différemment les différents types d'échecs et protège le reste du système contre une seule tâche défectueuse.
Deux types d'échecs
Commencez par diviser chaque erreur en deux catégories.
Les erreurs retryable sont transitoires. Un délai d'attente réseau vers une API tierce, une réponse 429 de limitation de débit, ou un réplica de base de données temporairement en retard sur son primaire. Ce sont des symptômes de pression, pas des bugs. Le système peut se rétablir de lui-même en trente secondes. Les tâches retryable méritent une nouvelle chance, mais seulement dans des conditions contrôlées.
Les erreurs permanentes sont des erreurs de logique. Un JSON invalide dans la charge utile, un ID utilisateur manquant, un fichier requis qui n'existe pas dans le stockage. Celles-ci échoueront à la centième tentative exactement comme elles ont échoué à la première. Les réessayer consomme des cycles CPU, gaspille des emplacements dans la file d'attente et crée une contre-pression (back-pressure) toxique qui retarde les tâches saines. Le seul endroit utile pour un échec permanent est un log, une alerte ou une dead-letter queue. Cela n'a pas sa place dans la boucle de réessai.
Construire un moteur de décision
Classifiez immédiatement. Ne laissez pas cette décision au framework de file d'attente. Dès que vous attrapez une erreur, décidez de son sort.
En pratique, cela signifie créer des classes d'erreur personnalisées ou des fonctions wrapper qui inspectent l'échec avant de le propager. Si un pilote de base de données renvoie une réinitialisation de connexion, votre gestionnaire doit la marquer comme retryable. Si un validateur de charge utile renvoie une erreur de schéma, marquez-la comme permanente. De nombreux processeurs de tâches réessaient tout par défaut, ce qui est le choix le plus coûteux que vous puissiez faire. Rejetez instantanément les tâches permanentes. Soit vous les abandonnez, soit vous les redirigez vers une dead-letter queue où elles ne pourront pas empoisonner le pipeline principal. Cette simple habitude prévient les effets de boule de neige plus efficacement que n'importe quel changement d'infrastructure.
Utiliser le backoff, mais plus intelligemment
Lorsque vous effectuez un réessai, ne le faites jamais immédiatement. Si une base de données est hors service, une salve de workers la martelant chaque seconde ressemble à
Concevez chaque tâche comme si elle devait s'exécuter deux fois, car cela peut arriver. Un worker peut échouer à mi-chemin du traitement, être replanifié et s'exécuter à nouveau. Si votre tâche facture un client, envoie un e-mail ou incrémente un stock, une tentative de réexécution naïve créera des doublons.
La solution est l'idempotence. Avant d'effectuer un effet de bord, vérifiez s'il a déjà eu lieu. Utilisez un identifiant unique provenant de la charge utile de la tâche comme clé d'idempotence. Stockez cette clé dans un cache à courte durée de vie ou dans une table de base de données avec une contrainte d'unicité. Si la clé existe, ignorez le travail et retournez un succès. Cela transforme les tentatives de réexécution, d'un risque, en une opération nulle (no-op) inoffensive. Cela demande quelques lignes de code supplémentaires, mais cela vous évitera d'expliquer à la finance pourquoi le chiffre d'affaires a doublé du jour au lendemain.
Protégez le processus
Les rejets de promesses non gérés et les exceptions imprévues peuvent tuer un processus Node.js sans avertissement. Pour un worker, cela signifie des tâches perdues et un orchestrateur qui s'efforce de redémarrer le conteneur.
Enregistrez des gestionnaires globaux pour unhandledRejection et uncaughtException. Leur rôle n'est pas de sauver l'application, mais d'effectuer le nettoyage minimal nécessaire, puis de quitter. Laissez Docker, Kubernetes ou systemd redémarrer le worker avec un état de mémoire propre. Continuer à fonctionner péniblement après le déclenchement d'un gestionnaire global favorise les fuites de mémoire et la corruption d'état. Une mort rapide et propre est préférable à un processus zombie lent qui traite les tâches de manière incorrecte. Faites confiance à votre orchestrateur pour vous relancer ; n'essayez pas de surpasser un runtime corrompu.
Respectez le signal
Les workers sont arrêtés lors des déploiements, des événements de mise à l'échelle et des rotations de nœuds. Si votre processus meurt à l'instant même où il reçoit un SIGTERM, vous interrompez la tâche en cours. Cette tâche pourrait ne jamais se terminer, et son compteur de tentatives n'a peut-être même pas encore été incrémenté.
Écoutez les signaux SIGTERM et SIGINT. Lorsqu'un signal arrive, arrêtez de récupérer de nouvelles tâches dans la file d'attente. Terminez la tâche actuelle si possible. Définissez un délai d'attente strict, par exemple trente secondes, après quoi vous quittez quoi qu'il arrive. Cet arrêt progressif respecte la file d'attente et évite les faux échecs. Votre pipeline de déploiement devrait considérer un worker qui s'arrête proprement comme étant "healthy", tandis qu'un worker qui plante devrait déclencher une alerte.
L'essentiel à retenir
La gestion fiable des files d'attente ne consiste pas à attraper chaque erreur. Il s'agit de prendre des décisions délibérées pour chaque mode de défaillance. Réessayez patiemment les erreurs transitoires. Enterrez rapidement les erreurs permanentes. Protégez vos workers contre les mouvements de foule, sécurisez vos données avec des clés d'idempotence et laissez les processus mourants s'arrêter proprement. Lorsque chaque échec suit un chemin défini, trois heures du matin ne devient qu'une heure comme les autres. Votre pipeline continue de tourner, et votre équipe continue de dormir.
