O tratamento de erros no Node.js precisa diferenciar falhas esperadas, bugs, indisponibilidade de dependências e erros fatais do processo. Colocar tudo dentro de um try/catch genérico pode esconder problemas, gerar respostas inconsistentes e manter a aplicação funcionando em estado corrompido.
Uma estratégia sólida cria erros tipados, preserva a causa original, mapeia falhas nas bordas, registra contexto seguro e decide quando repetir, responder, degradar ou encerrar o processo.
Neste guia, você aprenderá erros operacionais e de programação, classes customizadas, cause, promises, Express, HTTP Problem Details, filas, logs, observabilidade, shutdown e testes.
Como erros funcionam no Node.js?
O Node.js usa objetos Error e mecanismos diferentes conforme a API:
- exceções síncronas;
- Promise rejeitada;
- callback com primeiro argumento de erro;
- evento
errorem streams e EventEmitter; - códigos de saída do processo;
- erros do sistema com propriedades como
code.
A documentação oficial apresenta os erros no Node.js.
Erro operacional versus bug
Erros operacionais podem ocorrer mesmo com código correto:
- timeout;
- arquivo ausente;
- conexão recusada;
- registro não encontrado;
- entrada inválida;
- conflito de concorrência.
Erros de programação indicam defeito:
- acesso a propriedade de undefined;
- argumento impossível;
- invariante quebrada;
- estado interno inconsistente;
- Promise esquecida;
- tipo incorreto em código interno.
A resposta para cada grupo é diferente.
Classe base
export class ApplicationError extends Error {
constructor(
message: string,
readonly code: string,
readonly details?: unknown,
options?: ErrorOptions
) {
super(message, options);
this.name = new.target.name;
}
}O code é estável para decisões. A mensagem pode mudar.
Erros específicos
export class OrderNotFoundError
extends ApplicationError {
constructor(orderId: string) {
super(
'Pedido não encontrado',
'ORDER_NOT_FOUND',
{ orderId }
);
}
}
export class OrderConflictError
extends ApplicationError {
constructor(orderId: string) {
super(
'O pedido foi alterado por outra operação',
'ORDER_CONFLICT',
{ orderId }
);
}
}Não crie classes apenas para repetir mensagens. O tipo precisa orientar tratamento.
Error cause
try {
await provider.call(input);
} catch (error) {
throw new PaymentProviderError(
'Falha ao autorizar pagamento',
{ cause: error }
);
}A propriedade cause preserva a cadeia sem concatenar stacks manualmente.
Não exponha a causa ao cliente
O log interno pode incluir a cadeia. A resposta pública não deve revelar host, SQL, token ou stack.
Try/catch assíncrono
try {
const result = await operation();
return result;
} catch (error) {
handle(error);
}Sem await, o catch não captura a rejeição retornada:
try {
return operation();
} catch {
// não captura rejeição futura
}Não capture para relançar igual
try {
return await operation();
} catch (error) {
throw error;
}Esse bloco não agrega valor. Capture para adicionar contexto, traduzir ou limpar recursos.
Finally
const client = await pool.connect();
try {
return await execute(client);
} finally {
client.release();
}finally é ideal para liberar conexão, arquivo ou lock.
Não retorne em finally
Um return no finally pode substituir o resultado ou esconder uma exceção.
Promises paralelas
Promise.all rejeita quando uma falha:
const [user, permissions] = await Promise.all([
loadUser(id),
loadPermissions(id)
]);As outras operações não são automaticamente canceladas. Use AbortSignal quando necessário.
Promise.allSettled
Use quando todas as respostas são necessárias:
const results = await Promise.allSettled(tasks);
Depois trate cada status. Não ignore silenciosamente itens rejeitados.
Erros em callbacks
fs.readFile(path, (error, data) => {
if (error) {
return callback(error);
}
callback(null, data);
});Retorne após o erro para evitar callback duplo.
Streams
stream.on('error', error => {
logger.error({ err: error }, 'Falha no stream');
});Um EventEmitter sem listener de error pode encerrar o processo.
Pipeline
import { pipeline } from 'node:stream/promises';
await pipeline(source, transform, destination);A versão Promise facilita captura e destrói streams envolvidos em falha.
Erros do sistema
try {
await fs.readFile(path);
} catch (error) {
if (isNodeError(error) && error.code === 'ENOENT') {
return null;
}
throw error;
}Não classifique por texto da mensagem. Use code.
Type guard
function isNodeError(
error: unknown
): error is NodeJS.ErrnoException {
return error instanceof Error
&& 'code' in error;
}Em catch, trate o valor como unknown.
Normalização
function toError(value: unknown): Error {
if (value instanceof Error) return value;
return new Error(
typeof value === 'string'
? value
: 'Non-Error value thrown',
{ cause: value }
);
}JavaScript permite throw 'erro'. Normalize na borda.
Result Pattern
Falhas esperadas podem ser retornadas como Result em vez de exceção. Consulte Result Pattern no Node.js.
Quando usar Result
- validação normal;
- conflito esperado;
- parsing;
- regra de negócio negativa;
- ausência tratável.
Quando lançar
- contrato interno violado;
- dependência indisponível;
- configuração inválida;
- bug;
- falha que interrompe o fluxo atual.
Service Layer
Casos de uso podem lançar erros semânticos ou retornar Result. Controllers e consumidores mapeiam para o protocolo. Consulte Service Layer no Node.js.
Middleware do Express
app.use((error, req, res, next) => {
const normalized = toError(error);
if (normalized instanceof OrderNotFoundError) {
return res.status(404).json({
code: normalized.code,
message: normalized.message
});
}
logger.error({
err: normalized,
requestId: req.id
}, 'Erro não tratado');
res.status(500).json({
code: 'INTERNAL_ERROR',
message: 'Erro interno'
});
});O middleware deve ficar depois das rotas.
Express 5
Handlers async que rejeitam são encaminhados ao middleware de erro. Consulte Express 5: Erros Assíncronos.
HTTP Problem Details
A RFC 9457 define um formato padronizado, descrito em Problem Details for HTTP APIs.
{
"type": "https://api.example.com/problems/order-conflict",
"title": "Conflito no pedido",
"status": 409,
"detail": "O pedido foi alterado",
"instance": "/orders/123",
"code": "ORDER_CONFLICT"
}Mensagens públicas
Mensagens devem ser úteis e seguras. Não retorne:
- stack;
- consulta SQL;
- caminho interno;
- nome de servidor;
- credenciais;
- payload completo;
- mensagem bruta do fornecedor.
Status HTTP
- 400: formato inválido;
- 401: autenticação ausente ou inválida;
- 403: sem autorização;
- 404: recurso não encontrado;
- 409: conflito de estado;
- 412: precondição falhou;
- 422: dados semanticamente inválidos;
- 429: limite excedido;
- 500: erro inesperado;
- 502 ou 503: dependência indisponível;
- 504: timeout upstream.
Mantenha política consistente.
Erros de validação
return res.status(422).json({
code: 'VALIDATION_ERROR',
errors: [
{ field: 'email', code: 'INVALID_EMAIL' }
]
});Não retorne mensagens de biblioteca sem normalização.
PostgreSQL
Traduza códigos conhecidos:
23505: unique violation;23503: foreign key;40001: serialization failure;40P01: deadlock.
Não exponha esses códigos ao cliente. Consulte Transações PostgreSQL no Node.js.
Retries
Repita apenas falhas transitórias e operações idempotentes:
if (isRetryable(error) && attempt < maxAttempts) {
await delay(backoffWithJitter(attempt));
continue;
}Consulte Retry com Backoff no Node.js.
Timeouts
Timeout deve produzir erro próprio:
class DependencyTimeoutError extends ApplicationError {
constructor(service: string, options?: ErrorOptions) {
super(
'Dependência excedeu o tempo limite',
'DEPENDENCY_TIMEOUT',
{ service },
options
);
}
}AbortSignal
const signal = AbortSignal.timeout(3000);
await fetch(url, { signal });Diferencie cancelamento solicitado de timeout quando a resposta operacional precisar disso.
Circuit Breaker
Falhas repetidas podem abrir o circuito e retornar erro de indisponibilidade rapidamente. Consulte Circuit Breaker no Node.js.
Filas
Um consumidor decide:
- ack para sucesso;
- dead-letter para mensagem inválida;
- retry para falha transitória;
- alarme para bug inesperado.
Não repita permanentemente uma mensagem que nunca será válida.
Idempotência
Retries de mensagens e HTTP podem duplicar efeitos. Consulte Idempotência em APIs Node.js.
Logs estruturados
logger.error({
err: error,
requestId,
operation: 'approve_order',
orderId
}, 'Falha ao aprovar pedido');Use Pino ou Winston com redaction. Consulte Logs com Pino no Node.js.
Não registre duas vezes
Se cada camada registra o mesmo erro, haverá duplicidade. Uma convenção comum:
- camada interna adiciona contexto e relança;
- borda registra uma vez;
- erros esperados são métricas ou logs leves.
Stack trace
Stacks são úteis internamente, mas podem ficar grandes. Preserve em erros inesperados e evite enviá-las ao usuário.
Sentry
Capture bugs e falhas inesperadas, não cada 404 ou validação. Consulte Sentry no Node.js.
Métricas
Meça erros por operação e tipo controlado:
application_errors_total{
operation="approve_order",
type="dependency_timeout"
}Não use mensagem como label.
Unhandled rejection
Promises rejeitadas sem tratamento indicam bug. Registre e inicie shutdown controlado quando o estado puder estar inconsistente.
process.on('unhandledRejection', reason => {
fatalErrorHandler(toError(reason));
});Uncaught exception
process.on('uncaughtException', error => {
fatalErrorHandler(error);
});Não continue atendendo normalmente após exceção não capturada.
Fatal error handler
let shuttingDown = false;
async function fatalErrorHandler(error: Error) {
if (shuttingDown) return;
shuttingDown = true;
logger.fatal({ err: error }, 'Erro fatal');
process.exitCode = 1;
await shutdown({ timeoutMs: 10000 });
}Consulte Graceful Shutdown no Node.js.
Não chame process.exit imediatamente
Isso pode perder logs e interromper requests. Defina exitCode, feche recursos e use prazo máximo.
Domínios e invariantes
Uma invariante quebrada por dados internos pode ser fatal para a operação atual. Value Objects e entidades ajudam a impedir estados inválidos.
Consulte Value Objects no Node.js.
Erros e segurança
Mensagens diferentes para usuário existente e inexistente podem permitir enumeração. Em autenticação, use respostas neutras quando necessário.
Correlation ID
Inclua um ID na resposta e nos logs:
{
"code": "INTERNAL_ERROR",
"requestId": "req-..."
}O usuário pode informar o ID ao suporte sem receber detalhes internos.
Testes unitários
test('mapeia OrderNotFound para 404', async () => {
const response = await request(app)
.get('/orders/missing');
assert.equal(response.status, 404);
assert.equal(
response.body.code,
'ORDER_NOT_FOUND'
);
});Teste de causa
Simule erro do provider e confirme que o erro traduzido preserva cause no log, mas não na resposta.
Teste de timeout
Use um servidor falso lento. Confirme status, código, retry e cancelamento.
Teste de unhandled rejection
Teste o fatal handler como função injetável, sem derrubar o processo do runner. Confirme que inicia shutdown e define código de saída.
Fault injection
Provoque falhas em banco, cache, fila e filesystem. Verifique limpeza de recursos, rollback e logs.
Erros comuns
- Catch vazio: falha desaparece.
- Mensagem como decisão: tratamento quebra após tradução.
- Erro genérico: não há semântica.
- Stack ao cliente: informação interna vaza.
- Log em toda camada: duplicidade aumenta.
- Retry de bug: carga e dano crescem.
- Continuar após fatal: estado pode estar corrompido.
- Sem timeout: operações ficam penduradas.
Boas práticas
- Trate catch como unknown.
- Use códigos estáveis.
- Preserve cause.
- Diferencie esperado e inesperado.
- Mapeie erros nas bordas.
- Não exponha detalhes internos.
- Use timeouts e cancelamento.
- Repita apenas falhas transitórias.
- Registre uma vez com contexto.
- Encerre após erros fatais.
Conclusão
O tratamento de erros no Node.js é uma política arquitetural, não apenas blocos try/catch. Erros tipados, códigos estáveis, causas preservadas e mapeamento nas bordas tornam falhas previsíveis.
Com Result para caminhos esperados, exceções para falhas inesperadas, logs estruturados, timeouts e shutdown controlado, a aplicação responde com segurança e não esconde bugs críticos.




