Como lidar com erros de rede de forma elegante no frontend
1. Entendendo o cenário de falhas de rede
Erros de rede no frontend são inevitáveis e podem ocorrer por diversos motivos: timeout de conexão, falha de DNS, restrições de CORS, ou simplesmente o usuário estar offline. É fundamental distinguir entre erros de rede (falha na comunicação) e erros HTTP (respostas do servidor com status 4xx/5xx). Enquanto o segundo indica que a requisição chegou ao destino, o primeiro significa que nem isso aconteceu.
O impacto na experiência do usuário é direto: mensagens genéricas como "Algo deu errado" geram frustração e podem levar ao abandono da aplicação. Uma abordagem elegante transforma esses momentos de falha em oportunidades de confiança.
// Exemplo de captura básica de erro de rede vs erro HTTP
fetch('https://api.exemplo.com/dados')
.then(response => {
if (!response.ok) {
// Erro HTTP (4xx/5xx)
throw new Error(`Erro HTTP: ${response.status}`);
}
return response.json();
})
.catch(error => {
// Erro de rede ou exceção
console.error('Falha na comunicação:', error.message);
});
2. Estratégias de detecção e captura de erros
A detecção precisa é o primeiro passo para um tratamento elegante. Além do catch tradicional, podemos usar recursos nativos do navegador para identificar o estado da rede.
// Detecção de estado online/offline
function verificarConectividade() {
if (!navigator.onLine) {
mostrarMensagemOffline();
return false;
}
return true;
}
// Eventos de mudança de conectividade
window.addEventListener('online', () => {
sincronizarDadosPendentes();
fecharBannerOffline();
});
window.addEventListener('offline', () => {
exibirBannerOffline();
salvarEstadoAtual();
});
// Identificação de erros específicos
async function fazerRequisicao(url, options = {}) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 8000);
try {
const response = await fetch(url, {
...options,
signal: controller.signal
});
clearTimeout(timeoutId);
return response;
} catch (error) {
if (error.name === 'AbortError') {
throw new Error('Tempo limite excedido');
}
if (error instanceof TypeError) {
throw new Error('Falha de rede ou CORS');
}
throw error;
}
}
3. Feedback visual e comunicação com o usuário
A comunicação deve ser clara, amigável e acionável. Evite termos técnicos como "500 Internal Server Error" ou "Timeout". Em vez disso, use mensagens que orientem o usuário.
// Componente de notificação de erro reutilizável
function NotificacaoErro({ tipo, mensagem, acao, onRetry }) {
const estilos = {
erro: { cor: '#e74c3c', icone: '⚠️' },
aviso: { cor: '#f39c12', icone: '⚡' },
offline: { cor: '#95a5a6', icone: '📡' }
};
const estilo = estilos[tipo] || estilos.erro;
return (
<div style={{
background: estilo.cor,
color: 'white',
padding: '16px',
borderRadius: '8px',
marginBottom: '16px'
}}>
<span>{estilo.icone} {mensagem}</span>
{acao && (
<button onClick={acao} style={{ marginLeft: '12px' }}>
{acao.texto}
</button>
)}
{onRetry && (
<button onClick={onRetry} style={{ marginLeft: '12px' }}>
Tentar novamente
</button>
)}
</div>
);
}
// Estados de UI para diferentes cenários
const estadosUI = {
carregando: {
titulo: 'Buscando informações...',
descricao: 'Aguarde um momento',
acao: null
},
erro_rede: {
titulo: 'Sem conexão',
descricao: 'Verifique sua internet e tente novamente',
acao: { texto: 'Recarregar', funcao: () => window.location.reload() }
},
erro_servidor: {
titulo: 'Servidor indisponível',
descricao: 'Estamos trabalhando para resolver',
acao: { texto: 'Tentar mais tarde', funcao: () => {} }
}
};
4. Implementação de retry automático e backoff
Estratégias de retry com backoff evitam sobrecarregar o servidor e aumentam as chances de sucesso em falhas temporárias.
// Implementação de retry com backoff exponencial
async function requisicaoComRetry(url, options = {}, maxTentativas = 3) {
for (let tentativa = 1; tentativa <= maxTentativas; tentativa++) {
try {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
if (tentativa === maxTentativas) {
throw new Error('Falha após todas as tentativas');
}
// Backoff exponencial: 1s, 2s, 4s...
const delay = Math.pow(2, tentativa) * 1000;
console.log(`Tentativa ${tentativa} falhou. Nova tentativa em ${delay}ms`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
// Uso com configuração personalizada
async function buscarDadosComRetry() {
try {
const dados = await requisicaoComRetry(
'https://api.exemplo.com/dados',
{ method: 'GET' },
5 // máximo de 5 tentativas
);
return dados;
} catch (error) {
mostrarNotificacao({
tipo: 'erro',
mensagem: 'Não foi possível carregar os dados após várias tentativas'
});
}
}
5. Cache e fallback para modo offline
Armazenar dados localmente permite que a aplicação funcione parcialmente mesmo sem conexão.
// Serviço de cache com fallback offline
const cacheService = {
async buscar(chave, url) {
// Tentar buscar da rede primeiro
try {
const dados = await fetch(url).then(r => r.json());
this.salvar(chave, dados);
return dados;
} catch (erro) {
// Fallback para cache local
const dadosCache = this.recuperar(chave);
if (dadosCache) {
mostrarNotificacao({
tipo: 'aviso',
mensagem: 'Modo offline - exibindo dados salvos'
});
return dadosCache;
}
throw new Error('Sem dados disponíveis');
}
},
salvar(chave, dados) {
try {
localStorage.setItem(`cache_${chave}`, JSON.stringify({
dados,
timestamp: Date.now()
}));
} catch (e) {
console.warn('Falha ao salvar cache:', e);
}
},
recuperar(chave) {
const item = localStorage.getItem(`cache_${chave}`);
if (!item) return null;
const { dados, timestamp } = JSON.parse(item);
const expirado = Date.now() - timestamp > 3600000; // 1 hora
if (expirado) {
localStorage.removeItem(`cache_${chave}`);
return null;
}
return dados;
}
};
// Sincronização quando voltar online
async function sincronizarDadosPendentes() {
const pendentes = JSON.parse(
localStorage.getItem('operacoes_pendentes') || '[]'
);
for (const operacao of pendentes) {
try {
await fetch(operacao.url, {
method: operacao.metodo,
body: JSON.stringify(operacao.dados)
});
} catch (erro) {
console.error('Falha na sincronização:', erro);
}
}
localStorage.removeItem('operacoes_pendentes');
}
6. Tratamento de erros em operações críticas
Operações como formulários e pagamentos exigem cuidado redobrado para não perder dados do usuário.
// Gerenciamento de formulário com proteção contra perda de dados
class FormularioSeguro {
constructor() {
this.rascunho = null;
this.restaurarRascunho();
this.configurarAutoSave();
}
configurarAutoSave() {
// Salvar automaticamente a cada 30 segundos
setInterval(() => {
if (this.possuiDados()) {
this.salvarRascunho();
}
}, 30000);
}
async enviar(dados) {
try {
const resposta = await requisicaoComRetry(
'https://api.exemplo.com/enviar',
{ method: 'POST', body: JSON.stringify(dados) }
);
// Limpar rascunho após sucesso
localStorage.removeItem('rascunho_formulario');
mostrarSucesso('Dados enviados com sucesso!');
return resposta;
} catch (erro) {
// Salvar rascunho em caso de falha
this.salvarRascunho(dados);
mostrarNotificacao({
tipo: 'erro',
mensagem: 'Falha ao enviar. Seus dados foram salvos como rascunho.',
acao: {
texto: 'Tentar novamente',
funcao: () => this.enviar(dados)
}
});
throw erro;
}
}
salvarRascunho(dados = null) {
const dadosParaSalvar = dados || this.coletarDados();
localStorage.setItem('rascunho_formulario', JSON.stringify(dadosParaSalvar));
}
restaurarRascunho() {
const rascunho = localStorage.getItem('rascunho_formulario');
if (rascunho) {
this.rascunho = JSON.parse(rascunho);
mostrarNotificacao({
tipo: 'aviso',
mensagem: 'Rascunho recuperado. Seus dados anteriores foram restaurados.'
});
}
}
}
7. Boas práticas de arquitetura e testes
Centralizar o tratamento de erros facilita a manutenção e garante consistência.
// Hook personalizado para gerenciamento de requisições
function useRequisicaoSegura() {
const executar = async (url, opcoes = {}) => {
if (!navigator.onLine) {
throw new Error('Sem conexão com a internet');
}
try {
const resposta = await fetch(url, {
...opcoes,
headers: {
'Content-Type': 'application/json',
...opcoes.headers
}
});
if (!resposta.ok) {
throw new Error(`Erro do servidor: ${resposta.status}`);
}
return await resposta.json();
} catch (erro) {
// Log estruturado para debug
console.error({
tipo: 'erro_rede',
url,
metodo: opcoes.method || 'GET',
mensagem: erro.message,
timestamp: new Date().toISOString()
});
throw erro;
}
};
return { executar };
}
// Teste simulando falha de rede
async function testarComportamentoOffline() {
// Simular desconexão
Object.defineProperty(navigator, 'onLine', {
configurable: true,
get: () => false
});
try {
await fetch('https://api.exemplo.com/teste');
} catch (erro) {
console.assert(
erro.message.includes('Sem conexão'),
'Deveria detectar estado offline'
);
}
// Restaurar conectividade simulada
Object.defineProperty(navigator, 'onLine', {
configurable: true,
get: () => true
});
}
Referências
- MDN Web Docs: Fetch API — Documentação oficial sobre a API Fetch, incluindo tratamento de erros e AbortController
- Google Developers: Network Reliability Engineering — Guia sobre estratégias de cache e confiabilidade de rede
- Axios Documentation: Interceptors — Documentação oficial sobre interceptadores para tratamento centralizado de erros
- React Query Docs: Error Handling — Guia completo sobre tratamento de erros com React Query
- Service Workers: Offline Cookbook — Estratégias práticas para funcionamento offline com Service Workers
- Chrome DevTools: Network Throttling — Como simular condições de rede para testes
- OWASP: Error Handling Cheat Sheet — Boas práticas de segurança no tratamento de erros