Skip to content
Last updated

Lidar com erros

Estratégia geral

Classifique o erro antes de decidir a ação:

ClasseStatusAção
Erro do cliente4xx (exceto 429)Corrigir a requisição; não retentar sem mudança
Rate limit429Aguardar reset e retentar
Erro de servidor5xxRetentar com backoff exponencial
Conflito de estado409Verificar estado atual antes de retentar

Tratar 4xx

Erros 4xx indicam problema na sua requisição. Não faça retry sem corrigir a causa.

async function chamarApi(url: string, options: RequestInit) {
  const res = await fetch(url, options);

  if (res.status >= 400 && res.status < 500 && res.status !== 429) {
    const erro = await res.json();
    // Logar código de erro para diagnóstico
    console.error(`Erro ${res.status}: ${erro.erro} — ${erro.mensagem}`);
    throw new Error(`Erro de cliente: ${erro.erro}`);
  }

  return res;
}

Tratar 429 com backoff

async function chamarComBackoff(
  fn: () => Promise<Response>,
  maxTentativas = 4
): Promise<Response> {
  for (let i = 0; i < maxTentativas; i++) {
    const res = await fn();

    if (res.status !== 429) return res;

    const resetHeader = res.headers.get('X-RateLimit-Reset');
    const aguardar = resetHeader
      ? Math.max(Number(resetHeader) * 1000 - Date.now(), 0)
      : Math.pow(2, i) * 1000;

    console.warn(`Rate limit atingido. Aguardando ${aguardar}ms...`);
    await new Promise(r => setTimeout(r, aguardar));
  }
  throw new Error('Rate limit excedido após todas as tentativas');
}

Tratar 5xx com retry exponencial

async function chamarComRetry(
  fn: () => Promise<Response>,
  maxTentativas = 3
): Promise<Response> {
  const intervalos = [1000, 5000, 30000];

  for (let i = 0; i < maxTentativas; i++) {
    try {
      const res = await fn();
      if (res.status < 500) return res;
      console.warn(`Erro ${res.status} na tentativa ${i + 1}`);
    } catch (e) {
      console.warn(`Falha de rede na tentativa ${i + 1}:`, e);
    }

    if (i < maxTentativas - 1) {
      await new Promise(r => setTimeout(r, intervalos[i]));
    }
  }
  throw new Error('Serviço indisponível após retries');
}
Idempotência no retry

Erros 5xx não consomem a idempotencyKey. Reenvie a mesma chave no retry — é seguro e evita duplicatas.

Tratar 409 em pedidos

Um 409 Conflict em confirmação indica que o pedido já mudou de estado. Verifique o estado atual antes de retentar:

async function confirmarComVerificacao(pedidoId: string, key: string) {
  const res = await fetch(`${BASE_URL}/pedidos/${pedidoId}/confirmar`, {
    method: 'POST',
    headers: headers,
    body: JSON.stringify({ idempotencyKey: key }),
  });

  if (res.status === 409) {
    // Verificar estado atual
    const estadoRes = await fetch(`${BASE_URL}/pedidos/${pedidoId}`, { headers });
    const pedido = await estadoRes.json();
    console.log(`Pedido já em estado: ${pedido.status}`);
    // Se já confirmado, operação foi bem-sucedida — não é erro
    if (pedido.status === 'confirmado') return pedido;
    throw new Error(`Conflito inesperado: ${pedido.status}`);
  }

  return res.json();
}

Checklist de robustez

  • Todos os erros logam o campo erro (código de aplicação), não só o status HTTP.
  • Retry implementado com backoff exponencial para 5xx e 429.
  • idempotencyKey reutilizada nos retries de 5xx.
  • Erros 4xx lançam exceção sem retry (exceto 429).
  • Conflitos 409 verificam estado atual antes de agir.
  • Timeout de conexão configurado (recomendado: 10s).

Próximos passos