Most Node.js tutorials treat error handling as an afterthought. You wrap a route handler in a try/catch block, log the stack trace, and return a 500. That mindset survives because a real person is waiting on the other end of an HTTP request. Background jobs are different. In a queue system, there is no impatient client to reply to, no automatic browser refresh. There is only a worker, a payload, and a retry counter ticking upward in silence. When things go wrong, they go wrong slowly, then all at once. One misclassified error can stall an entire pipeline or page an engineer at three in the morning.
The disconnect is simple. Request-response cycles fail fast and fail loud. A queue failure is quiet. A worker might churn through hundreds of jobs before a database connection drops. Without clear handling rules, the worker retries instantly, hammers the already struggling database, and crashes. Because no one is watching the worker directly, the first sign of trouble is often a cascading backup or a disk full of log files. You need more than catch blocks. You need a strategy that treats different failures differently and protects the rest of the system from a single bad job.
Two Kinds of Failure
Start by splitting every error into one of two buckets.
Retryable errors are transient. A network timeout to a third-party API, a 429 rate-limit response, or a temporary database replica lagging behind its primary. These are symptoms of pressure, not bugs. The system might heal itself in thirty seconds. Retryable jobs deserve another shot, but only under controlled conditions.
Permanent errors are mistakes. Invalid JSON in the payload, a missing user ID, a required file that does not exist in storage. These will fail on the hundredth attempt exactly as they failed on the first. Retrying them burns CPU cycles, wastes queue slots, and creates toxic back-pressure that delays healthy jobs. The only useful place for a permanent failure is a log, an alert, or a dead-letter queue. It does not belong in the retry loop.
Build a Decision Engine
Classify immediately. Do not leave this decision to the queue framework. The moment you catch an error, decide its fate.
In practice, this means creating custom error classes or wrapper functions that inspect the failure before bubbling it up. If a database driver throws a connection reset, your handler should tag that as retryable. If a payload validator throws a schema mismatch, tag it as permanent. Many job processors default to retrying everything, which is the most expensive choice you can make. Reject permanent jobs instantly. Either drop them or reroute them to a dead-letter queue where they cannot poison the main pipeline. This single habit prevents snowball effects more reliably than any infrastructure change.
Back Off, But Smarter
When you do retry, never do it immediately. If a database is down, a burst of workers hammering it every second looks like a denial-of-service attack from the inside. Use exponential backoff. Wait one minute, then five, then fifteen. Give the upstream system room to recover.
But exponential backoff alone is not enough. If a thousand jobs fail at the same time because a service restarted, their retry schedules will align. They will hit that service simultaneously when it comes back online, potentially knocking it down again. Add jitter: a small random offset to each delay. Scattering retries across a few seconds prevents synchronized stampedes. The math is simple, but the stability it buys is enormous.
Preserve the Evidence
A dead-letter queue is your audit trail, not a trash bin. When a job exhausts its final retry, do not simply delete it. Move the entire payload, along with the error context and the retry history, to a DLQ.
This preserves evidence. A human can inspect the job, patch the bug, and replay it manually if necessary. More importantly, monitor the depth of your DLQ. A sudden spike in dead-lettered jobs is often the earliest warning of a bad deploy, a schema change gone wrong, or an external vendor breaking their contract. Treat DLQ growth as a leading indicator, not a trailing one. If your DLQ is filling up, something upstream has changed and your team needs to know before the backlog spreads.
Design for the Retry
Diseña cada tarea como si fuera a ejecutarse dos veces, porque podría ocurrir. Un trabajador puede fallar a mitad del procesamiento, ser reprogramado y ejecutarse de nuevo. Si tu tarea realiza un cargo a un cliente, envía un correo electrónico o incrementa un recuento de inventario, un reintento ingenuo creará duplicados.
La solución es la idempotencia. Antes de realizar un efecto secundario, comprueba si ya ha ocurrido. Utiliza un identificador único de la carga útil de la tarea como clave de idempotencia. Almacena esa clave en una caché de corta duración o en una tabla de base de datos con una restricción de unicidad. Si la clave existe, omite el trabajo y devuelve un éxito. Esto convierte los reintentos de un riesgo en una operación nula (no-op) inofensiva. Requiere unas pocas líneas de código adicionales, pero te evita tener que explicar al departamento de finanzas por qué los ingresos se duplicaron de la noche a la mañana.
Protege el proceso
Los rechazos de promesas sin control y las excepciones errantes pueden matar un proceso de Node.js sin previo aviso. En un trabajador, eso significa tareas perdidas y un orquestador luchando por reiniciar el contenedor.
Registra manejadores globales para unhandledRejection y uncaughtException. Su función no es rescatar la aplicación. Es realizar la limpieza mínima necesaria y luego salir. Deja que Docker, Kubernetes o systemd reinicien el trabajador con un estado de memoria limpio. Seguir funcionando a duras penas después de que se active un manejador global invita a fugas de memoria y estados corruptos. Una muerte rápida y limpia es más segura que un proceso zombie lento que procesa tareas incorrectamente. Confía en que tu orquestador te traerá de vuelta; no intentes ser más listo que un entorno de ejecución corrupto.
Respeta la señal
Los trabajadores se apagan durante los despliegues, eventos de escalado y rotaciones de nodos. Si tu proceso muere en el instante en que recibe SIGTERM, abortarás cualquier tarea que esté en curso. Es posible que esa tarea nunca termine y que su contador de reintentos ni siquiera se haya incrementado todavía.
Escucha SIGTERM y SIGINT. Cuando llegue una señal, deja de extraer nuevas tareas de la cola. Termina la tarea actual si puedes. Establece un tiempo de espera estricto, quizás de treinta segundos, tras el cual saldrás sin importar qué. Este cierre ordenado respeta la cola y evita fallos falsos. Tu canalización de despliegue debería tratar a un trabajador que sale limpiamente como saludable, mientras que un trabajador que ha fallado debería activar una alerta.
La verdadera conclusión
El manejo fiable de colas no consiste en capturar cada error. Consiste en tomar decisiones deliberadas para cada modo de fallo. Reintenta los transitorios con paciencia. Entierra los permanentes rápidamente. Protege a tus trabajadores de estampidas, protege tus datos con claves de idempotencia y deja que los procesos que mueren salgan limpiamente. Cuando cada fallo tiene un camino definido, las tres de la mañana se convierten en simplemente otra hora más. Tu canalización sigue avanzando y tu equipo sigue durmiendo.
