Executando verificação de segurança...
1

Fetch, Retry e Timeout: O Código que Falta entre Sua API e a Realidade

Resumo rápido

Este post mostra como construir, do zero, um client HTTP resiliente em JavaScript/TypeScript usando a Fetch API nativa. Sem axios, sem got, sem ky. Cubro retry com backoff exponencial, timeouts com AbortController, interceptors de request/response, tratamento de erros por status code e tipagem forte com generics. Cada bloco de código é funcional e copy-paste ready.

O bug de 4 horas que me fez reescrever tudo

Em março de 2023, um serviço Node.js que eu mantinha para um cliente de e-commerce começou a retornar erros 503 intermitentes. O serviço consumia uma API de cálculo de frete de terceiro. O código era esse:

// ❌ O código que estava em produção
const response = await fetch('https://frete-api.exemplo.com/calculate', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(payload),
});
const data = await response.json();

Sem timeout. Sem retry. Sem checagem de response.ok. O fetch da Fetch API não rejeita a Promise em respostas 4xx/5xx. Ele resolve normalmente. Então o código seguia feliz com um body de erro parseado como se fosse o cálculo de frete. O resultado: pedidos sendo finalizados com frete zero. Quatro horas de vendas com frete grátis involuntário. O prejuízo foi de R$ 23.400.

A partir desse incidente, eu parei de tratar consumo de API como detalhe de implementação e comecei a tratar como infraestrutura crítica.

Por que o fetch nativo é suficiente (e por que axios não é mais necessário)

O axios resolveu problemas reais em 2016: interceptors, transformação automática de JSON, cancelamento de requests e um wrapper para XMLHttpRequest no browser. Em 2025, a Fetch API cobre quase tudo isso nativamente:

FuncionalidadeaxiosFetch API nativaDesde quando
PromisesSempre
Rejeição em 4xx/5xx✅ automático❌ manual
CancelamentoCancelToken (deprecated) / AbortControllerAbortControllerNode 18+ / Browsers 2019+
Streaming de responseParcial✅ ReadableStreamNode 18+
Interceptors✅ built-in❌ manual
Timeout nativo✅ config.timeout❌ via AbortSignal.timeout()Node 18.11+
Bundle size~13kB min+gzip0kB (nativo)

O que falta no fetch é conveniência, não capacidade. E conveniência a gente constrói em 150 linhas de código, sob nosso controle total.

Eu removi axios de 3 projetos em produção entre 2023 e 2024. Em um deles, um dashboard Next.js com 47 chamadas de API distintas, o bundle do client caiu de 312kB para 299kB (gzipped). Treze kilobytes parecem pouco até você lembrar que cada kilobyte conta no LCP de dispositivos móveis com 3G. O tempo de parse do JavaScript no Chrome caiu de 1.8s para 1.6s em um Moto G22 real.

A fundação: fetch que realmente verifica erros

O primeiro passo é criar uma função que trate response.ok de verdade. Eu chamo essa camada de safeFetch:

// src/http/safe-fetch.ts

export class HttpError extends Error {
  constructor(
    public readonly status: number,
    public readonly statusText: string,
    public readonly body: unknown,
    public readonly url: string,
  ) {
    super(`HTTP ${status} ${statusText} at ${url}`);
    this.name = 'HttpError';
  }
}

export async function safeFetch<T>(
  url: string,
  init?: RequestInit,
): Promise<T> {
  const response = await fetch(url, init);

  if (!response.ok) {
    // Tenta parsear o body de erro para dar contexto ao chamador.
    // APIs bem feitas retornam JSON com detalhes mesmo em 4xx/5xx.
    let errorBody: unknown;
    try {
      errorBody = await response.json();
    } catch {
      errorBody = await response.text().catch(() => null);
    }

    throw new HttpError(
      response.status,
      response.statusText,
      errorBody,
      url,
    );
  }

  // Respostas 204 No Content não têm body
  if (response.status === 204) {
    return undefined as T;
  }

  return response.json() as Promise<T>;
}

Essa função já resolve 80%


Leia o artigo completo em https://vivodecodigo.com.br/backend/fetch-retry-timeout-client-http-resiliente-javascript

Carregando publicação patrocinada...