Javascript

Tratamento de erros em requisições HTTP Já leu

12 min de leitura

Tratamento de erros em requisições HTTP
Fazer uma requisição funcionar no ambiente de desenvolvimento é fácil. O verdadeiro desafio é fazer sua aplicação se comportar bem quando as coisas dão errado — e elas sempre dão. O servidor fica fora do ar. A internet c

Fazer uma requisição funcionar no ambiente de desenvolvimento é fácil. O verdadeiro desafio é fazer sua aplicação se comportar bem quando as coisas dão errado — e elas sempre dão. O servidor fica fora do ar. A internet cai. O token expira. A API retorna dados inesperados. O usuário está com conexão 2G em um trem.

Um código que só funciona quando tudo está certo não está pronto para produção. Este artigo ensina a construir um sistema de requisições robusto que lida graciosamente com qualquer tipo de falha.


Os tipos de erro em requisições HTTP

Antes de tratar, precisamos classificar:

┌─────────────────────────────────────────────────────┐
│              TIPOS DE ERRO EM REQUISIÇÕES           │
│                                                     │
│  1. Erros de REDE                                   │
│     - Sem internet                                  │
│     - DNS não resolvido                             │
│     - Timeout de conexão                            │
│     - CORS bloqueado                                │
│     → Fetch REJEITA a Promise (TypeError)           │
│                                                     │
│  2. Erros HTTP (servidor respondeu, mas com erro)   │
│     - 400 Bad Request                               │
│     - 401 Unauthorized                              │
│     - 403 Forbidden                                 │
│     - 404 Not Found                                 │
│     - 422 Unprocessable Entity                      │
│     - 429 Too Many Requests                         │
│     - 500 Internal Server Error                     │
│     - 503 Service Unavailable                       │
│     → Fetch RESOLVE a Promise (response.ok = false) │
│                                                     │
│  3. Erros de PARSE                                  │
│     - JSON malformado                               │
│     - Tipo inesperado de resposta                   │
│     → response.json() REJEITA a Promise             │
│                                                     │
│  4. Erros de NEGÓCIO                                │
│     - Validação falhou                              │
│     - Regra de negócio violada                      │
│     - Recurso em estado inválido                    │
│     → status 200 mas com campo de erro no corpo     │
└─────────────────────────────────────────────────────┘

Erros HTTP — o que cada código significa

const ERROS_HTTP = {
  // 4xx — Erros do cliente
  400: "Requisição inválida — verifique os dados enviados.",
  401: "Não autenticado — faça login para continuar.",
  403: "Acesso negado — você não tem permissão para esta ação.",
  404: "Recurso não encontrado.",
  405: "Método HTTP não permitido.",
  408: "Timeout — o servidor demorou para responder.",
  409: "Conflito — o recurso já existe ou está em estado incompatível.",
  410: "Recurso removido permanentemente.",
  422: "Dados inválidos — verifique os campos do formulário.",
  429: "Muitas requisições — aguarde antes de tentar novamente.",

  // 5xx — Erros do servidor
  500: "Erro interno do servidor — tente novamente mais tarde.",
  502: "Gateway inválido — serviço intermediário com problema.",
  503: "Serviço indisponível — servidor sobrecarregado ou em manutenção.",
  504: "Timeout do gateway — o servidor demorou para responder.",
};

function mensagemDeErro(status) {
  return ERROS_HTTP[status] || `Erro inesperado (${status}).`;
}

Criando uma classe de erro HTTP personalizada

class ErroHTTP extends Error {
  constructor(status, mensagem, dados = null) {
    super(mensagem);
    this.name = "ErroHTTP";
    this.status = status;
    this.dados = dados; // corpo da resposta de erro, se houver
  }

  get eClientError() { return this.status >= 400 && this.status < 500; }
  get eServerError() { return this.status >= 500; }
  get eNaoAutorizado() { return this.status === 401; }
  get eProibido() { return this.status === 403; }
  get eNaoEncontrado() { return this.status === 404; }
  get eMuitasRequisicoes() { return this.status === 429; }
}

class ErroRede extends Error {
  constructor(mensagem = "Falha de rede. Verifique sua conexão.") {
    super(mensagem);
    this.name = "ErroRede";
  }
}

class ErroTimeout extends Error {
  constructor(ms) {
    super(`A requisição excedeu o tempo limite de ${ms}ms.`);
    this.name = "ErroTimeout";
  }
}

class ErroParse extends Error {
  constructor(mensagem) {
    super(mensagem);
    this.name = "ErroParse";
  }
}

O cliente HTTP robusto

Vamos construir um cliente HTTP completo que centraliza todo o tratamento de erros:

class ClienteHTTP {
  constructor(baseURL = "", opcoesPadrao = {}) {
    this.baseURL = baseURL;
    this.opcoesPadrao = {
      headers: {
        "Content-Type": "application/json",
        "Accept": "application/json",
      },
      timeout: 10000, // 10 segundos
      ...opcoesPadrao,
    };
    this.interceptadores = {
      requisicao: [],
      resposta: [],
      erro: [],
    };
  }

  // ── Interceptadores ─────────────────────────────

  adicionarInterceptadorRequisicao(fn) {
    this.interceptadores.requisicao.push(fn);
  }

  adicionarInterceptadorResposta(fn) {
    this.interceptadores.resposta.push(fn);
  }

  adicionarInterceptadorErro(fn) {
    this.interceptadores.erro.push(fn);
  }

  // ── Requisição principal ─────────────────────────

  async requisitar(endpoint, opcoes = {}) {
    const url = `${this.baseURL}${endpoint}`;

    // Mescla opções
    let configuracao = {
      ...this.opcoesPadrao,
      ...opcoes,
      headers: {
        ...this.opcoesPadrao.headers,
        ...opcoes.headers,
      },
    };

    // Aplica interceptadores de requisição
    for (const interceptador of this.interceptadores.requisicao) {
      configuracao = await interceptador(configuracao);
    }

    // Timeout com AbortController
    const { timeout, ...fetchOpcoes } = configuracao;
    const controlador = new AbortController();
    const idTimeout = setTimeout(() => controlador.abort(), timeout);

    try {
      const response = await fetch(url, {
        ...fetchOpcoes,
        signal: controlador.signal,
      });

      clearTimeout(idTimeout);

      // Trata erros HTTP
      if (!response.ok) {
        let dadosErro = null;

        try {
          dadosErro = await response.json();
        } catch {
          // Sem corpo JSON no erro
        }

        const mensagem = dadosErro?.mensagem
          || dadosErro?.message
          || dadosErro?.error
          || mensagemDeErro(response.status);

        throw new ErroHTTP(response.status, mensagem, dadosErro);
      }

      // Sem conteúdo
      if (response.status === 204) return null;

      // Parse do corpo
      const contentType = response.headers.get("content-type") || "";
      let dados;

      try {
        dados = contentType.includes("application/json")
          ? await response.json()
          : await response.text();
      } catch (e) {
        throw new ErroParse(`Falha ao parsear resposta: ${e.message}`);
      }

      // Aplica interceptadores de resposta
      let resultado = dados;
      for (const interceptador of this.interceptadores.resposta) {
        resultado = await interceptador(resultado, response);
      }

      return resultado;

    } catch (erro) {
      clearTimeout(idTimeout);

      // Transforma erros conhecidos
      if (erro.name === "AbortError") {
        throw new ErroTimeout(timeout);
      }

      if (erro instanceof TypeError && !erro instanceof ErroHTTP) {
        throw new ErroRede(erro.message);
      }

      // Aplica interceptadores de erro
      let erroFinal = erro;
      for (const interceptador of this.interceptadores.erro) {
        erroFinal = await interceptador(erroFinal) || erroFinal;
      }

      throw erroFinal;
    }
  }

  // ── Métodos HTTP ────────────────────────────────

  get(endpoint, opcoes = {}) {
    return this.requisitar(endpoint, { ...opcoes, method: "GET" });
  }

  post(endpoint, corpo, opcoes = {}) {
    return this.requisitar(endpoint, {
      ...opcoes,
      method: "POST",
      body: JSON.stringify(corpo),
    });
  }

  put(endpoint, corpo, opcoes = {}) {
    return this.requisitar(endpoint, {
      ...opcoes,
      method: "PUT",
      body: JSON.stringify(corpo),
    });
  }

  patch(endpoint, corpo, opcoes = {}) {
    return this.requisitar(endpoint, {
      ...opcoes,
      method: "PATCH",
      body: JSON.stringify(corpo),
    });
  }

  delete(endpoint, opcoes = {}) {
    return this.requisitar(endpoint, { ...opcoes, method: "DELETE" });
  }
}

Configurando o cliente com interceptadores

// Criando a instância do cliente
const api = new ClienteHTTP("https://jsonplaceholder.typicode.com");

// Interceptador de requisição — adiciona token de autenticação
api.adicionarInterceptadorRequisicao((config) => {
  const token = localStorage.getItem("token");
  if (token) {
    config.headers["Authorization"] = `Bearer ${token}`;
  }
  console.log(`[API] ${config.method || "GET"} ${config.url || ""}`);
  return config;
});

// Interceptador de resposta — logging
api.adicionarInterceptadorResposta((dados, response) => {
  console.log(`[API] ✅ ${response.status} — ${response.url}`);
  return dados;
});

// Interceptador de erro — tratamento global
api.adicionarInterceptadorErro(async (erro) => {
  if (erro instanceof ErroHTTP) {
    // Token expirado — redireciona para login
    if (erro.eNaoAutorizado) {
      localStorage.removeItem("token");
      console.warn("[API] Sessão expirada. Redirecionando para login...");
      // window.location.href = "/login";
    }

    // Rate limiting — aguarda e tenta novamente
    if (erro.eMuitasRequisicoes) {
      console.warn("[API] Rate limit atingido. Aguardando 5s...");
      await new Promise(r => setTimeout(r, 5000));
    }

    console.error(`[API] ❌ Erro ${erro.status}: ${erro.message}`);
  }

  if (erro instanceof ErroRede) {
    console.error("[API] ❌ Sem conexão:", erro.message);
  }

  if (erro instanceof ErroTimeout) {
    console.error("[API] ⏰ Timeout:", erro.message);
  }

  return erro; // repropaga
});

Retry automático com backoff exponencial

async function comRetry(fn, opcoes = {}) {
  const {
    tentativas = 3,
    delayBase = 1000,
    fatorMultiplicador = 2,
    errosRetentaveis = [408, 429, 500, 502, 503, 504],
    aoTentar = null,
  } = opcoes;

  let ultimoErro;

  for (let tentativa = 1; tentativa <= tentativas; tentativa++) {
    try {
      return await fn();

    } catch (erro) {
      ultimoErro = erro;

      // Verifica se vale a pena tentar de novo
      const deveRetentar =
        erro instanceof ErroRede ||
        erro instanceof ErroTimeout ||
        (erro instanceof ErroHTTP && errosRetentaveis.includes(erro.status));

      if (!deveRetentar || tentativa === tentativas) {
        throw erro;
      }

      // Calcula delay com backoff exponencial + jitter
      const delay = delayBase * Math.pow(fatorMultiplicador, tentativa - 1);
      const jitter = Math.random() * 200; // evita thundering herd
      const espera = Math.round(delay + jitter);

      if (aoTentar) {
        aoTentar(tentativa, tentativas, espera, erro);
      } else {
        console.warn(`[Retry] Tentativa ${tentativa}/${tentativas} falhou. Aguardando ${espera}ms...`);
      }

      await new Promise(r => setTimeout(r, espera));
    }
  }

  throw ultimoErro;
}

// Uso
async function buscarComRetry(id) {
  return comRetry(
    () => api.get(`/users/${id}`),
    {
      tentativas: 3,
      delayBase: 1000,
      aoTentar: (atual, total, espera, erro) => {
        console.warn(`Tentativa ${atual}/${total} — ${erro.message} — aguardando ${espera}ms`);
      },
    }
  );
}

const usuario = await buscarComRetry(1);

Circuit Breaker — protegendo o sistema

O Circuit Breaker é um padrão que "abre o circuito" quando muitas falhas acontecem, evitando sobrecarregar um serviço que já está com problemas:

class CircuitBreaker {
  constructor(opcoes = {}) {
    this.limite = opcoes.limite || 5;        // falhas para abrir
    this.timeout = opcoes.timeout || 60000;  // ms até tentar fechar
    this.falhas = 0;
    this.ultimaFalha = null;
    this.estado = "fechado"; // fechado | aberto | semi-aberto
  }

  async executar(fn) {
    if (this.estado === "aberto") {
      const tempoPassado = Date.now() - this.ultimaFalha;

      if (tempoPassado > this.timeout) {
        this.estado = "semi-aberto";
        console.log("[CircuitBreaker] Semi-aberto — testando serviço...");
      } else {
        throw new Error(
          `[CircuitBreaker] Circuito aberto. Aguarde ${Math.ceil((this.timeout - tempoPassado) / 1000)}s.`
        );
      }
    }

    try {
      const resultado = await fn();

      // Sucesso — fecha o circuito
      if (this.estado === "semi-aberto") {
        this.resetar();
        console.log("[CircuitBreaker] Serviço recuperado. Circuito fechado.");
      }

      return resultado;

    } catch (erro) {
      this.falhas++;
      this.ultimaFalha = Date.now();

      if (this.falhas >= this.limite) {
        this.estado = "aberto";
        console.error(`[CircuitBreaker] Circuito ABERTO após ${this.falhas} falhas.`);
      }

      throw erro;
    }
  }

  resetar() {
    this.falhas = 0;
    this.ultimaFalha = null;
    this.estado = "fechado";
  }

  get estaAberto() { return this.estado === "aberto"; }
}

// Uso
const breaker = new CircuitBreaker({ limite: 3, timeout: 30000 });

async function buscarComBreaker(id) {
  return breaker.executar(() => api.get(`/users/${id}`));
}

Tratamento de erros na interface

Todo erro deve ter um tratamento visual adequado:

// Sistema de feedback de erros para o usuário
const UI = {
  mostrarErro(erro, contexto = "") {
    let mensagemUsuario;
    let acao = null;

    if (erro instanceof ErroHTTP) {
      switch (erro.status) {
        case 401:
          mensagemUsuario = "Sua sessão expirou. Faça login novamente.";
          acao = { label: "Fazer login", fn: () => window.location.href = "/login" };
          break;
        case 403:
          mensagemUsuario = "Você não tem permissão para esta ação.";
          break;
        case 404:
          mensagemUsuario = `${contexto || "O recurso"} não foi encontrado.`;
          break;
        case 422:
          mensagemUsuario = erro.dados?.erros
            ? `Dados inválidos:
${erro.dados.erros.join("
")}`
            : "Os dados enviados são inválidos.";
          break;
        case 429:
          mensagemUsuario = "Muitas tentativas. Aguarde alguns segundos.";
          break;
        case 500:
        case 503:
          mensagemUsuario = "Problema no servidor. Tente novamente em instantes.";
          acao = { label: "Tentar novamente", fn: () => window.location.reload() };
          break;
        default:
          mensagemUsuario = erro.message || "Algo deu errado.";
      }
    } else if (erro instanceof ErroRede) {
      mensagemUsuario = "Sem conexão com a internet. Verifique sua rede.";
    } else if (erro instanceof ErroTimeout) {
      mensagemUsuario = "A requisição demorou muito. Tente novamente.";
      acao = { label: "Tentar novamente", fn: () => window.location.reload() };
    } else {
      mensagemUsuario = "Erro inesperado. Por favor, tente novamente.";
    }

    this.exibirToast(mensagemUsuario, "erro", acao);
    console.error("[UI Error]", erro); // log técnico para o desenvolvedor
  },

  exibirToast(mensagem, tipo = "info", acao = null) {
    const toast = document.createElement("div");
    toast.className = `toast toast-${tipo}`;
    toast.innerHTML = `
      <span>${mensagem}</span>
      ${acao ? `<button onclick="${acao.fn}">${acao.label}</button>` : ""}
    `;

    document.body.appendChild(toast);
    setTimeout(() => toast.remove(), 5000);
  },

  mostrarLoading(elemento, ativo) {
    if (ativo) {
      elemento.disabled = true;
      elemento.dataset.textoOriginal = elemento.textContent;
      elemento.textContent = "Carregando...";
    } else {
      elemento.disabled = false;
      elemento.textContent = elemento.dataset.textoOriginal || "Confirmar";
    }
  },
};

Exemplo completo — formulário com tratamento robusto

const btnSalvar = document.querySelector("#btn-salvar");
const form = document.querySelector("#form-usuario");

form.addEventListener("submit", async (e) => {
  e.preventDefault();
  UI.mostrarLoading(btnSalvar, true);

  const dados = Object.fromEntries(new FormData(form).entries());

  try {
    const usuario = await comRetry(
      () => api.post("/users", dados),
      { tentativas: 2, delayBase: 800 }
    );

    UI.exibirToast(`Usuário ${usuario.name} criado com sucesso!`, "sucesso");
    form.reset();

  } catch (erro) {
    UI.mostrarErro(erro, "Usuário");

  } finally {
    UI.mostrarLoading(btnSalvar, false);
  }
});

Checklist de tratamento de erros em produção

✅ Verificar response.ok em toda requisição fetch
✅ Distinguir erros de rede, HTTP e parse
✅ Nunca expor mensagens técnicas ao usuário final
✅ Sempre logar o erro completo no console (ou serviço de monitoramento)
✅ Implementar retry para erros transitórios (500, 503, timeout)
✅ Não fazer retry em erros permanentes (400, 401, 403, 404)
✅ Exibir feedback visual adequado para cada tipo de erro
✅ Manter o estado do formulário em caso de erro (não limpar os campos)
✅ Implementar timeout em todas as requisições
✅ Usar AbortController em buscas ao vivo
✅ Tratar o caso de ausência de internet (offline)
✅ Testar os estados de erro, não só o caminho feliz

Tarefa para você

Estenda o ClienteHTTP com um sistema de cache em memória:

// Adicione ao ClienteHTTP:
// - cache com tempo de expiração configurável
// - método get() verifica cache antes de fazer a requisição
// - método invalidarCache(endpoint) para limpar manualmente
// - opção { cache: false } para forçar requisição fresca

const api = new ClienteHTTP("https://jsonplaceholder.typicode.com", {
  cacheTTL: 30000, // 30 segundos
});

// Primeira chamada — vai para a rede
const u1 = await api.get("/users/1");

// Segunda chamada — retorna do cache instantaneamente
const u2 = await api.get("/users/1");

// Forçar requisição nova
const u3 = await api.get("/users/1", { cache: false });

Conclusão

Neste artigo você aprendeu:

  • Os quatro tipos de erro em requisições HTTP
  • O significado dos principais códigos de status HTTP
  • Como criar classes de erro personalizadas e semânticas
  • Como construir um cliente HTTP robusto com interceptadores
  • Retry com backoff exponencial e jitter
  • O padrão Circuit Breaker para proteção do sistema
  • Como traduzir erros técnicos em mensagens úteis para o usuário
  • Um checklist completo para produção

No próximo artigo vamos explorar APIs públicas na prática — com exemplos reais de integração com diferentes tipos de serviços.


 

📚 Fontes e Referências

  • MDN Web Docs — HTTP Status Codes: https://developer.mozilla.org/pt-BR/docs/Web/HTTP/Status
  • MDN Web Docs — Fetch API: https://developer.mozilla.org/pt-BR/docs/Web/API/Fetch_API
  • MDN Web Docs — AbortController: https://developer.mozilla.org/en-US/docs/Web/API/AbortController
  • JavaScript.info — Fetch: Error Handling: https://javascript.info/fetch-api
  • Martin Fowler — Circuit Breaker Pattern: https://martinfowler.com/bliki/CircuitBreaker.html
  • Google — Exponential Backoff: https://cloud.google.com/storage/docs/retry-strategy
  • You Don't Know JS: Async & Performance — Kyle Simpson: https://github.com/getify/You-Dont-Know-JS
  • Node.js Design Patterns — Mario Casciaro (Packt Publishing)
  • web.dev — Resilient patterns for network requests: https://web.dev/resilient-search-experiences
Comentários

Mais em Javascript

ESLint e Prettier: código limpo e padronizado
ESLint e Prettier: código limpo e padronizado

Imagine entrar em um projeto onde cada arquivo tem um estilo diferente — aspa...

O que é Programação Assíncrona? O Event Loop Explicado
O que é Programação Assíncrona? O Event Loop Explicado

Este &eacute; um dos artigos mais importantes de toda a s&eacute;rie. N&atild...

O que é JavaScript e por que aprender?
O que é JavaScript e por que aprender?

Se voc&ecirc; quer trabalhar com desenvolvimento web &mdash; seja criando sit...