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