A maioria dos tutoriais de Node.js trata o tratamento de erros como algo secundário. Você envolve um manipulador de rota em um bloco try/catch, registra o stack trace e retorna um 500. Essa mentalidade sobrevive porque existe uma pessoa real esperando do outro lado de uma requisição HTTP. Trabalhos em segundo plano (background jobs) são diferentes. Em um sistema de fila, não há um cliente impaciente para responder, nem um refresh automático do navegador. Existe apenas um worker, um payload e um contador de tentativas aumentando silenciosamente. Quando as coisas dão errado, elas dão errado lentamente e, depois, todas de uma vez. Um erro mal classificado pode travar um pipeline inteiro ou acordar um engenheiro às três da manhã.

O descompasso é simples. Ciclos de requisição-resposta falham rápido e de forma barulhenta. Uma falha de fila é silenciosa. Um worker pode processar centenas de jobs antes que uma conexão de banco de dados caia. Sem regras claras de tratamento, o worker tenta novamente de forma instantânea, sobrecarrega o banco de dados que já está sofrendo e trava. Como ninguém está observando o worker diretamente, o primeiro sinal de problema costuma ser um acúmulo em cascata ou um disco cheio de arquivos de log. Você precisa de mais do que blocos catch. Você precisa de uma estratégia que trate diferentes falhas de formas diferentes e proteja o resto do sistema de um único job problemático.

Dois Tipos de Falha

Comece dividindo cada erro em um de dois grupos.

Erros reexecutáveis (retryable) são transitórios. Um timeout de rede em uma API de terceiros, uma resposta 429 de limite de taxa (rate-limit) ou uma réplica de banco de dados temporariamente atrasada em relação à primária. Estes são sintomas de pressão, não bugs. O sistema pode se recuperar sozinho em trinta segundos. Jobs reexecutáveis merecem outra tentativa, mas apenas sob condições controladas.

Erros permanentes são equívocos. Um JSON inválido no payload, um ID de usuário ausente, um arquivo obrigatório que não existe no storage. Estes falharão na centésima tentativa exatamente como falharam na primeira. Reexecutá-los consome ciclos de CPU, desperdiça slots de fila e cria uma contrapressão (back-pressure) tóxica que atrasa jobs saudáveis. O único lugar útil para uma falha permanente é um log, um alerta ou uma dead-letter queue. Ela não pertence ao loop de reexecução.

Construa um Mecanismo de Decisão

Classifique imediatamente. Não deixe essa decisão para o framework de fila. No momento em que você capturar um erro, decida o destino dele.

Na prática, isso significa criar classes de erro customizadas ou funções wrapper que inspecionam a falha antes de propagá-la. Se um driver de banco de dados lançar um "connection reset", seu manipulador deve marcá-lo como reexecutável. Se um validador de payload lançar um erro de incompatibilidade de esquema (schema mismatch), marque-o como permanente. Muitos processadores de jobs, por padrão, tentam reexecutar tudo, o que é a escolha mais cara que você pode fazer. Rejeite jobs permanentes instantaneamente. Ou descarte-os ou redirecione-os para uma dead-letter queue, onde não possam envenenar o pipeline principal. Esse único hábito previne efeitos de bola de neve de forma mais confiável do que qualquer mudança de infraestrutura.

Recue, mas de Forma Inteligente

Quando você for reexecutar, nunca o faça imediatamente. Se um banco de dados estiver fora do ar, uma enxurrada de workers martelando-o a cada segundo parecerá um ataque de negação de serviço (DoS) vindo de dentro. Use backoff exponencial. Espere um minuto, depois cinco, depois quinze. Dê espaço para o sistema de upstream se recuperar.

Mas o backoff exponencial sozinho não é suficiente. Se mil jobs falharem ao mesmo tempo porque um serviço reiniciou, seus cronogramas de reexecução estarão alinhados. Eles atingirão esse serviço simultaneamente quando ele voltar a ficar online, potencialmente derrubando-o novamente. Adicione jitter: um pequeno deslocamento aleatório (offset) em cada atraso. Espalhar as tentativas de reexecução ao longo de alguns segundos evita estampidas sincronizadas. A matemática é simples, mas a estabilidade que ela proporciona é enorme.

Preserve as Evidências

Uma dead-letter queue (DLQ) é sua trilha de auditoria, não uma lixeira. Quando um job esgotar sua última tentativa, não o delete simplesmente. Mova todo o payload, junto com o contexto do erro e o histórico de tentativas, para uma DLQ.

Isso preserva evidências. Um humano pode inspecionar o job, corrigir o bug e reexecutá-lo manualmente, se necessário. Mais importante ainda, monitore a profundidade da sua DLQ. Um pico repentino em jobs enviados para a DLQ é frequentemente o aviso mais precoce de um deploy malfeito, uma mudança de esquema que deu errado ou um fornecedor externo quebrando seu contrato. Trate o crescimento da DLQ como um indicador antecedente, não um indicador de atraso. Se sua DLQ está enchendo, algo no upstream mudou e sua equipe precisa saber antes que o backlog se espalhe.

Projete para o Retry

Projete cada tarefa como se ela fosse ser executada duas vezes, porque isso pode acontecer. Um worker pode falhar no meio do processamento, ser reagendado e executar novamente. Se a sua tarefa cobra um cliente, envia um e-mail ou incrementa uma contagem de estoque, uma tentativa de repetição (retry) ingênua criará duplicatas.

A solução é a idempotência. Antes de realizar um efeito colateral, verifique se ele já ocorreu. Use um identificador único do payload da tarefa como uma chave de idempotência. Armazene essa chave em um cache de curta duração ou em uma tabela de banco de dados com uma restrição de unicidade. Se a chave existir, pule o trabalho e retorne sucesso. Isso transforma os retries de um risco em um no-op inofensivo. Exige algumas linhas extras de código, mas evita que você tenha que explicar ao financeiro por que a receita dobrou da noite para o dia.

Proteja o Processo

Rejeições de promises sem limite e exceções perdidas podem matar um processo Node.js sem aviso prévio. Em um worker, isso significa tarefas perdidas e um orquestrador tentando desesperadamente reiniciar o container.

Registre handlers globais para unhandledRejection e uncaughtException. O trabalho deles não é resgatar a aplicação. É realizar a limpeza mínima necessária e, em seguida, sair. Deixe o Docker, Kubernetes ou systemd reiniciar o worker com um estado de memória limpo. Continuar operando precariamente após o disparo de um handler global convida a vazamentos de memória e estados corrompidos. Uma morte rápida e limpa é mais segura do que um processo zumbi lento que processa tarefas incorretamente. Confie no seu orquestrador para trazê-lo de volta; não tente ser mais esperto que um runtime corrompido.

Respeite o Sinal

Workers são encerrados durante deployments, eventos de escalonamento e rotações de nós. Se o seu processo morrer no instante em que receber um SIGTERM, você abortará qualquer tarefa que esteja em execução. Essa tarefa pode nunca terminar, e seu contador de retries pode nem ter sido incrementado ainda.

Escute por SIGTERM e SIGINT. Quando um sinal chegar, pare de buscar novas tarefas na fila. Conclua a tarefa atual, se possível. Defina um timeout rígido, talvez trinta segundos, após o qual você sairá independentemente de qualquer coisa. Esse desligamento gracioso (graceful shutdown) respeita a fila e evita falhas falsas. Seu pipeline de deployment deve tratar um worker que encerra de forma limpa como saudável, enquanto um worker que falhou deve disparar um alerta.

A Grande Lição

O manuseio confiável de filas não consiste em capturar todos os erros. Trata-se de tomar decisões deliberadas para cada modo de falha. Tente novamente os erros transitórios com paciência. Enterre os permanentes rapidamente. Proteja seus workers de stampedes, proteja seus dados com chaves de idempotência e deixe que processos em encerramento saiam de forma limpa. Quando cada falha tem um caminho definido, as três da manhã tornam-se apenas mais uma hora. Seu pipeline continua avançando e sua equipe continua dormindo.