Menu

Tratamento de erros no Express: error middleware e async

Um error handler do Express é um middleware com quatro argumentos, (err, req, res, next), registrado depois das suas rotas. O Express 5 envia para ele erros lançados, chamadas de next(err) e promises rejeitadas de handlers async.

Esta página tem editores executáveis. Edite, execute e veja a saída na hora.

O Express trata erros com um middleware especial que recebe quatro argumentos, (err, req, res, next), e é registrado depois de todas as suas rotas. O Express envia para ele todo erro lançado dentro de uma rota, todo erro que você passa para next(err) e, no Express 5, toda promise rejeitada de um handler async. O handler decide o status code e envia uma única resposta de erro, então o resto do seu código pode simplesmente lançar.

GET /users/99 responde 404 com {"error":"No user with id 99"}, e o Console mostra a linha do error handler. Envie GET /users/1 e a rota responde normalmente; o error handler só roda quando algo dá errado.

Erros que o Express captura para você

Há três jeitos de chegar ao error handler a partir de uma rota, e no Express 5 os três funcionam igual:

GET /async responde 500 com {"error":"Rejected in an async route"}; GET /sync e GET /next terminam no mesmo handler com as próprias mensagens. Agora remova next dos parâmetros do error handler e envie GET /sync de novo: com três parâmetros o Express o trata como um middleware comum, pula ele e responde com a página de erro HTML padrão.

Erros async: Express 5 vs Express 4

A rota /async funciona porque o Express 5 captura a promise rejeitada. O Express 4 não capturava: o erro virava uma unhandled rejection e a requisição ficava pendurada.

Express 4Express 5
throw em um handler síncronoCapturadoCapturado
next(err)CapturadoCapturado
Promise rejeitada de um handler asyncNão capturado, a requisição travaCapturado
Erro em um setTimeout ou callback de eventoNão capturado, o processo caiNão capturado, o processo cai

No Express 4, envolva cada handler async ou instale o pacote express-async-errors:

// Express 4 only
const asyncHandler = (fn) => (req, res, next) =>
    Promise.resolve(fn(req, res, next)).catch(next);

app.get('/users/:id', asyncHandler(async (req, res) => {
    const user = await db.findUser(req.params.id);
    res.json(user);
}));

A última linha é igual nas duas versões: o Express não enxerga um erro lançado em um callback que ele não chamou. Transforme APIs de callback em promises (quase todo o Node tem uma versão com promise, como fs/promises) e dê await nelas.

Um handler 404 e um erro JSON consistente

Uma API é mais fácil de usar quando todo erro tem o mesmo formato. Dê um status aos erros, transforme rotas sem correspondência em um erro 404 e deixe um único handler formatar todos:

GET /nope não casa com nenhuma rota, então o handler 404 transforma a requisição em erro e a resposta é {"error":"Cannot GET /nope","status":404}. Envie GET /users/99 e o erro lançado pela rota ganha o mesmo formato. Envie POST /users com o body {"name": }: o express.json() não consegue interpretar e chama next() com um erro cujo status é 400, então a resposta é {"error":"Unexpected token '}', \"{\"name\": }\" is not valid JSON","status":400}.

Não vaze stack traces

As mensagens de erro do seu próprio HttpError são escritas para o cliente. Mensagens e stacks de todo o resto (um driver de banco, um erro de digitação que lança TypeError) podem revelar caminhos de arquivos, queries e versões de bibliotecas. O handler acima envia Internal server error para todo 5xx e registra o erro real no servidor. if (res.headersSent) return next(err); cobre um erro que acontece depois de parte da resposta já ter sido enviada: a única saída segura nesse caso é deixar o Express fechar a conexão.

O handler padrão do próprio Express segue o NODE_ENV: ele inclui o stack trace na página HTML, a menos que NODE_ENV seja production. Defina essa variável no seu servidor de produção (NODE_ENV=production node server.js), mas não dependa disso: um handler personalizado que nunca envia err.stack é mais seguro.

Erros comuns

Registrar o error handler antes das rotas. O Express só passa erros para frente, então um handler acima de uma rota nunca vê os erros dela. Coloque ele por último, depois do handler 404.

Capturar um erro e não fazer nada com ele. Um try/catch que só registra o log deixa a requisição pendurada. No catch, chame next(err), lance de novo ou envie uma resposta.

Enviar duas respostas. Lançar um erro depois de res.json() faz o error handler tentar enviar de novo. Confira res.headersSent, como no exemplo.

Para entender como funcionam o middleware comum e o next(), veja middleware no Express.

Perguntas frequentes

Posso ter mais de um error handler no Express?

Sim. Eles rodam em ordem, e cada um envia uma resposta ou chama next(err) para passar o erro adiante. Uma divisão comum é um handler que registra o log e outro que envia a resposta.

O que acontece se um error handler lançar um erro?

O Express passa o erro novo para o próximo error handler e, se não houver, para o seu handler nativo, que envia uma página 500. Mantenha os error handlers simples para que eles mesmos não possam falhar.

Que status code uma resposta de erro deve usar?

Use 4xx quando o cliente enviou algo errado (400 entrada inválida, 401 sem login, 403 sem permissão, 404 não encontrado, 409 conflito) e 500 quando o seu código falhou. Nunca responda 200 com um erro no body.

Como retorno erros de validação de vários campos?

Responda 400 com uma lista, por exemplo { "errors": [{ "field": "email", "message": "is required" }] }. Com o zod, converta result.error.issues para esse formato no error handler, assim toda rota informa entradas inválidas do mesmo jeito.

Ilustração das linguagens de programação do Coddy

Aprenda a programar com o Coddy

Começar