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