Async/Await e Tratamento de Erros no Node.js
1. Visão Geral
async/await te permite escrever código assíncrono no Node.js que parece código síncrono, muito mais legível do que callbacks ou cadeias de .then(). Mas se você não entender bem o mecanismo por trás, é fácil escrever a captura de erros de forma incorreta, levando a UnhandledPromiseRejection que derruba toda a aplicação.
Este artigo vai desde como async/await funciona, padrões corretos de tratamento de erros, até erros comuns ao combinar com Promise.all, loops e middleware Express. Ao final, você saberá como escrever funções async seguras e debugar erros assíncronos mais rapidamente.
2. Como Async/Await Funciona
Uma função declarada como async sempre retorna uma Promise, não importa qual valor você return dentro dela. await apenas “pausa” no ponto de chamada, cedendo o controle ao event loop, sem bloquear a thread.
async function getUser(id) {
const response = await fetch(`https://api.example.com/users/${id}`);
const user = await response.json();
return user;
}
getUser(1).then(user => console.log(user.name));
A linha await fetch(...) aguarda a Promise do fetch resolver antes de prosseguir. Se fetch rejeitar (por exemplo, sem conexão), o erro será lançado exatamente naquele ponto await — e se você não capturar, ele rejeitará a Promise que getUser retorna.
Nota: Uma
async functionsempre retorna uma Promise, mesmo que você nãoawaitnada dentro dela. Chamar semawaitou.then()é a causa mais comum de bugs “código não executa na ordem certa”.
3. Capturar Erros com try/catch
A forma mais correta de tratar erros em uma função async é envolver os pontos await com try/catch.
async function getOrder(orderId) {
try {
const order = await orderRepository.findById(orderId);
if (!order) {
throw new Error(`Order ${orderId} not found`);
}
return order;
} catch (err) {
console.error('Failed to get order:', err.message);
throw err; // re-lançar para o chamador saber e continuar tratando
}
}
tryenvolve toda a lógica que pode lançar ou rejeitar.catch (err)recebe tanto erros dethrowmanual quanto rejeições de Promise.throw errno final para não “engolir” o erro — o chamador ainda recebe a Promise rejeitada.
Nota: Se você
catchum erro mas não o re-lança e nãoreturnum valor válido, a função async resolverá comundefined. O chamador acredita que tudo está bem, mas os dados estão faltando — um bug muito difícil de detectar.
3.1. Classe de Erro Customizada
Para categorizar erros claramente (erros de negócio, erros de sistema…), é bom criar uma classe de erro própria.
class PaymentError extends Error {
constructor(message, code) {
super(message);
this.name = 'PaymentError';
this.code = code;
}
}
async function chargeCard(order) {
const result = await paymentGateway.charge(order.total);
if (!result.success) {
throw new PaymentError('Card declined', 'CARD_DECLINED');
}
return result;
}
Ao capturar erros, você pode verificar err instanceof PaymentError para tratar diferentes tipos de erro, em vez de comparar strings de mensagem que são propensas a erros.
4. Erros em Promise.all e Promise.allSettled
Promise.all executa várias Promises em paralelo, mas basta uma rejeitar e todas são rejeitadas imediatamente, as Promises restantes ainda executam “em background” mas seus resultados são descartados.
async function notifyCustomers(customers) {
const results = await Promise.all(
customers.map(c => emailService.send(c.email))
);
return results;
}
Se emailService.send do cliente 2 falhar, toda a função rejeita imediatamente — você não sabe se os clientes 1, 3, 4 tiveram envio bem-sucedido ou não.
Quando você quer saber o resultado de todas as Promises, mesmo que falhem, use Promise.allSettled:
async function notifyCustomersSafely(customers) {
const results = await Promise.allSettled(
customers.map(c => emailService.send(c.email))
);
const failed = results.filter(r => r.status === 'rejected');
if (failed.length > 0) {
console.warn(`${failed.length} email(s) failed to send`);
}
return results;
}
Promise.allSettled nunca rejeita. Cada elemento em results tem a forma { status: 'fulfilled', value } ou { status: 'rejected', reason }. Você decide como tratar depois.
[
{ status: 'fulfilled', value: true },
{ status: 'rejected', reason: Error: SMTP timeout },
{ status: 'fulfilled', value: true }
]
5. Erros Comuns ao Usar Await em Loops
Usar await em forEach é um erro muito comum — o callback do forEach não aguarda async.
// ERRADO: forEach não pode ser aguardado
async function processOrders(orders) {
orders.forEach(async order => {
await orderService.process(order);
});
console.log('Done'); // executa imediatamente, não aguarda os pedidos serem processados
}
forEach chama o callback async depois o ignora a Promise retornada, não aguarda resolver. console.log('Done') executa antes de qualquer pedido ser processado — se process lançar um erro, esse erro não será capturado em lugar algum, causando UnhandledPromiseRejection.
Corrija usando for...of se você precisa processar sequencialmente:
async function processOrdersSequentially(orders) {
for (const order of orders) {
await orderService.process(order);
}
console.log('Done');
}
Ou use Promise.all com map se quiser processar em paralelo:
async function processOrdersParallel(orders) {
await Promise.all(orders.map(order => orderService.process(order)));
console.log('Done');
}
6. Tratamento de Erros em Middleware Express
Express por padrão não captura erros de funções async em route handlers — se você lançar um erro dentro, a requisição ficará pendurada ou o processo vai travar em versões antigas do Node (Express 4 não captura automaticamente rejeição de Promise).
// ARRISCADO: erros async não são capturados automaticamente pelo Express
app.get('/orders/:id', async (req, res) => {
const order = await orderService.getOrder(req.params.id);
res.json(order);
});
A forma segura é escrever um wrapper que passa o erro para next():
function asyncHandler(fn) {
return (req, res, next) => {
fn(req, res, next).catch(next);
};
}
app.get('/orders/:id', asyncHandler(async (req, res) => {
const order = await orderService.getOrder(req.params.id);
if (!order) {
const err = new Error('Order not found');
err.statusCode = 404;
throw err;
}
res.json(order);
}));
app.use((err, req, res, next) => {
res.status(err.statusCode || 500).json({ message: err.message });
});
asyncHandler envolve o route handler, se a Promise retornada for rejeitada, .catch(next) passa o erro para o middleware de tratamento de erros do Express em vez de deixá-lo flutuando.
Nota: Express 5 (ainda em beta no momento em que este artigo foi escrito) já captura automaticamente rejeições de async handlers, mas como Express 4 ainda é amplamente usado, você ainda precisa deste wrapper.
7. Timeout e AbortController
Um erro fácil de ignorar: aguardar uma Promise que nunca resolve deixará a função pendurada indefinidamente. Use AbortController para limitar o tempo de espera.
async function fetchWithTimeout(url, timeoutMs = 3000) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, { signal: controller.signal });
return await response.json();
} catch (err) {
if (err.name === 'AbortError') {
throw new Error(`Request to ${url} timed out after ${timeoutMs}ms`);
}
throw err;
} finally {
clearTimeout(timer);
}
}
setTimeout chama controller.abort() após timeoutMs, fazendo fetch rejeitar com um erro chamado AbortError. finally sempre limpa o timer para evitar vazamento de memória, sucesso ou falha.
8. Escrever Testes para Funções Async
Use Jest para testar casos de sucesso e erro.
<!-- package.json (seção dependencies, não é Maven) -->
npm install --save-dev jest
// orderService.js
async function getOrder(orderId, repository) {
const order = await repository.findById(orderId);
if (!order) {
throw new Error(`Order ${orderId} not found`);
}
return order;
}
module.exports = { getOrder };
// orderService.test.js
const { getOrder } = require('./orderService');
describe('getOrder', () => {
it('retorna order quando encontrado', async () => {
const mockRepo = { findById: jest.fn().mockResolvedValue({ id: 1, total: 100 }) };
const order = await getOrder(1, mockRepo);
expect(order).toEqual({ id: 1, total: 100 });
});
it('lança erro quando order não é encontrado', async () => {
const mockRepo = { findById: jest.fn().mockResolvedValue(null) };
await expect(getOrder(99, mockRepo)).rejects.toThrow('Order 99 not found');
});
});
mockResolvedValue simula uma Promise resolvendo com um valor predefinido. rejects.toThrow verifica que a Promise é rejeitada com a mensagem esperada — essa é a forma correta de testar erros assíncronos, em vez de envolver try/catch manualmente no teste.
PASS ./orderService.test.js
getOrder
✓ retorna order quando encontrado (3 ms)
✓ lança erro quando order não é encontrado (2 ms)
9. Erros Comuns para Evitar
- Esquecer
await: chamar uma função async semawaitfaz o código não executar na ordem correta, e erros são “engolidos” silenciosamente. - Misturar
.then()eawait: causa confusão, escolha um estilo para cada função. try/catchmuito amplo: envolver lógica não relacionada a async torna difícil identificar de onde vem o erro.- Não re-lançar em catch: faz o chamador receber
undefinedem vez do erro, causando bugs silenciosos na camada anterior. - Usar
forEachcom callback async: não aguarda, usefor...ofouPromise.all.
Nota: Se sua lógica não tem nenhuma I/O ou tarefa assíncrona, não precise marcar a função como
async— apenas aumenta o overhead de criar uma Promise desnecessariamente.
10. Conclusão
async/await torna o código assíncrono no Node.js fácil de ler, mas o tratamento correto de erros é a parte crucial para que a aplicação execute com estabilidade. Sempre envolva try/catch ao redor dos pontos await, re-lance erros quando necessário, use Promise.allSettled quando quiser saber o resultado de todas as tarefas paralelas, e não esqueça o wrapper para route handlers do Express. Escrever testes para casos de sucesso e erro lhe dá mais confiança ao refatorar lógica assíncrona depois.