Quando sviluppi con Node.js, la gestione degli errori sembra quasi troppo semplice all'inizio. Avvolgi una rotta in un try-catch, invii un codice di stato 500 e il client decide cosa fare successivamente. Questo modello funziona bene per HTTP, ma crolla non appena ci si sposta verso i job in background. In un sistema di code, non c'è un client in attesa. C'è solo un worker, un payload e un contatore di tentativi che aumenta da qualche parte in Redis, RabbitMQ o SQS. Se gestisci i fallimenti nello stesso modo in cui gestisci le richieste web fallite, non perderai solo una transazione. Bloccherai l'intera pipeline, consumerai risorse di calcolo o farai crashare i tuoi worker ripetutamente sullo stesso messaggio avvelenato.
La mentalità HTTP fallisce nei job in background
In un ciclo di richiesta-risposta, il ciclo di feedback è immediato. Un utente clicca un pulsante, il server lancia un errore e l'utente vede una schermata di errore. La pulizia è semplice. Un worker di una coda vive in isolamento. Preleva un job, ci lavora per secondi o minuti e poi conferma il successo. Se qualcosa va storto nel mezzo, la coda non ha idea del perché. Sa solo che l'acknowledgment non è mai arrivato. A seconda della tua configurazione, riproverà, forse per sempre. Un singolo payload malformato può rimbalzare tra i worker centinaia di volte, sprecando CPU e nascondendosi dietro job legittimi che hanno effettivamente bisogno di essere elaborati.
Due tipi di fallimento
La prima regola delle code resilienti è smettere di trattare ogni errore allo stesso modo. Devi dividere i fallimenti in due gruppi nell'istante stesso in cui si verificano.
I fallimenti riproducibili sono transitori. Pensa ai timeout di rete, ai limiti di frequenza (rate limits) di un'API di terze parti o a una connessione al database che si è resettata perché il pool era momentaneamente esaurito. Questi sono sintomi di un sistema vivo sotto pressione. Potrebbero avere successo al prossimo tentativo tra due minuti.
I fallimenti permanenti sono "poison pills". Questi includono payload malformati, errori di validazione dello schema o un campo obbligatorio mancante perché un servizio a monte ha cambiato il suo contratto. Riprovare in questi casi è puro spreco. Falliranno in modo identico al centesimo tentativo.
Se il tuo blocco catch non riesce a distinguere tra questi due, la tua coda sta volando alla cieca.
Pattern 1: Classifica gli errori nel blocco catch
Il blocco catch del tuo worker dovrebbe essere il codice più ponderato del file. Quando emerge un errore, ispezionalo immediatamente. Il codice di errore è ECONNRESET o un timeout? Mettilo in coda per un nuovo tentativo. È un SyntaxError, un rifiuto di validazione di Joi o un vincolo di chiave esterna mancante? Spostalo direttamente in una dead-letter queue, o DLQ, e non contarlo nel limite di tentativi.
La maggior parte delle librerie di code per Node.js, tra cui BullMQ e Bee Queue, ti permette di definire strategie di backoff personalizzate e hook per gli errori. Usale. Un errore permanente non dovrebbe mai attendere e riprovare tre volte per impostazione predefinita. Dovrebbe essere evacuato dalla coda principale in modo che il resto dei tuoi job possa scorrere. La DLQ preserva il payload esatto e il contesto dell'errore, il che ti consente di riprodurre il job in seguito, dopo aver risolto il bug o corretto lo schema.
Pattern 2: Exponential Backoff con Jitter
Riprovare istantaneamente è un approccio aggressivo. Se un database a valle sta già faticando sotto carico, colpirlo di nuovo ogni due secondi da cinquanta worker lo finirà. Devi fare un passo indietro e dare al sistema spazio per recuperare.
Usa l'exponential backoff. Al primo fallimento, attendi un secondo. Al secondo, attendi due. Poi quattro, poi otto, fino a un limite ragionevole come cinque minuti. Ma il solo tempismo non è sufficiente. Se ogni job fallito utilizza lo stesso identico intervallo, collideranno tutti quando il backoff scade. Quella onda sincronizzata, a volte chiamata "thundering herd", può sopraffare un servizio in fase di recupero.
Aggiungi il jitter. Prendi il ritardo calcolato e applica una variazione casuale, magari dal dieci al venti per cento. Quattro secondi diventano 4,2 o 4,7. Questa semplice casualità spalma il picco di tentativi e impedisce alla tua infrastruttura di subire colpi a ondate.
Pattern 3: Progetta per l'idempotenza
È qui che l'infrastruttura delle code incontra la logica di business. Immagina un job che addebita un cliente tramite un provider di pagamenti. Il worker esegue l'addebito con successo, ma la connessione cade prima di poter registrare il successo nel database o confermare il job. La coda vede un fallimento. Riprova. Il cliente viene addebitato due volte.
In Node.js, evita questo problema rendendo ogni effetto collaterale idempotente. Genera una chiave di idempotenza a partire dall'ID del job o da un identificatore specifico del business. Prima di creare l'addebito, inviare l'email o regolare l'inventario, verifica se il lavoro è già stato svolto. Passa quella chiave fino al database e a qualsiasi API di terze parti che la accetti. Struttura i tuoi job in modo che l'esecuzione dello stesso payload dieci volte produca lo stesso risultato di un'unica esecuzione. Questa singola abitudine elimina un'intera categoria di bug finanziari e di integrità dei dati.
Pattern 4: Tratta la tua Dead-Letter Queue come una dashboard
Una DLQ non è un cimitero dove i job errati vanno a essere dimenticati. È uno strumento operativo e dovrebbe essere una delle tue superfici più monitorate.
Configura degli alert che scattino quando la profondità della DLQ aumenta. Anche un singolo messaggio nella DLQ spesso significa che la logica di validazione è interrotta, che uno schema upstream è cambiato o che un servizio downstream sta inviando dati spazzatura che non riconosci più. Questi sono esattamente i segnali che vuoi intercettare prima che i clienti inizino a lamentarsi. Crea una dashboard che ti permetta di ispezionare il payload grezzo, lo stack trace e il timestamp. Tieni pronto un runbook: ispeziona il fallimento, applica la patch al codice e poi riproduci i messaggi nell'ordine corretto. Se la tua implementazione della coda lo supporta, imposta alert sia sul tasso di crescita che sul conteggio assoluto, perché un deploy errato può inondare la DLQ in pochi minuti.
Pattern 5: In caso di dubbio, fai crashare il processo
Node.js gira su un singolo event loop all'interno di un isolate V8. Un unhandled promise rejection o un
