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 function sempre retorna uma Promise, mesmo que você não await nada dentro dela. Chamar sem await ou .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
  }
}
  • try envolve toda a lógica que pode lançar ou rejeitar.
  • catch (err) recebe tanto erros de throw manual quanto rejeições de Promise.
  • throw err no final para não “engolir” o erro — o chamador ainda recebe a Promise rejeitada.

Nota: Se você catch um erro mas não o re-lança e não return um valor válido, a função async resolverá com undefined. 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 sem await faz o código não executar na ordem correta, e erros são “engolidos” silenciosamente.
  • Misturar .then() e await: causa confusão, escolha um estilo para cada função.
  • try/catch muito 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 undefined em vez do erro, causando bugs silenciosos na camada anterior.
  • Usar forEach com callback async: não aguarda, use for...of ou Promise.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.

nodejsasync-awaitjavascripterror-handlingpromise