Monitorar erros em produção é essencial para manter a qualidade de qualquer aplicação web. Diferente do ambiente de desenvolvimento, onde temos acesso ao console e podemos reproduzir erros facilmente, em produção os usuários encontram problemas que não conseguimos reproduzir localmente. Sem uma estratégia de monitoramento, erros passam despercebidos, levando a falhas silenciosas, perda de dados e experiência ruim. Nesta aula, vamos explorar técnicas para capturar erros globais em JavaScript, incluindo o evento window.onerror e unhandledrejection, e discutir ferramentas que facilitam o rastreamento e a análise de erros.

Vamos começar entendendo como o JavaScript lida com erros e como podemos interceptá-los antes que cheguem ao usuário. Em seguida, mergulharemos em exemplos práticos e boas práticas para implementar um monitoramento eficaz.

window.onerror

O evento window.onerror é um dos mecanismos mais antigos para capturar erros não tratados em JavaScript. Ele é disparado sempre que uma exceção não é capturada por nenhum bloco try/catch e propaga até o escopo global. A assinatura da função de callback recebe vários parâmetros que fornecem informações detalhadas sobre o erro.

Ao utilizar window.onerror, você pode registrar o erro em um serviço de monitoramento ou enviá-lo para um endpoint próprio. É importante notar que erros de sintaxe não são capturados por esse evento, pois eles ocorrem antes da execução do script. Além disso, erros em scripts de outros domínios (cross-origin) podem ser mascarados como 'Script error.' a menos que você configure o atributo crossorigin nos scripts e os servidores enviem os cabeçalhos CORS adequados.

// Exemplo de uso de window.onerror
window.onerror = function(message, source, lineno, colno, error) {
  console.error('Erro capturado:', { message, source, lineno, colno, error });
  // Enviar para serviço de monitoramento
  sendErrorToMonitoring({ message, source, lineno, colno, error });
  return true; // evita que o erro seja exibido no console padrão
};

// Simulando um erro não tratado
throw new Error('Este erro será capturado por window.onerror');

No exemplo acima, a função sendErrorToMonitoring seria uma função que envia os dados para um backend ou serviço de terceiros. O retorno true indica que o erro foi tratado, impedindo que o console padrão exiba o erro (embora em alguns navegadores isso não funcione mais).

Outra forma de usar window.onerror é atribuí-lo via propriedade ou usar addEventListener. A diferença é que com addEventListener você pode ter múltiplos ouvintes, enquanto a propriedade sobrescreve qualquer ouvinte anterior.

// Usando addEventListener
window.addEventListener('error', (event) => {
  const { message, filename, lineno, colno, error } = event;
  console.log('Erro capturado via addEventListener');
  // ...
});

É importante notar que o evento error também é disparado para erros de recursos (como imagens ou scripts) que falham ao carregar. Para distinguir entre erros de execução e erros de recurso, você pode verificar se o alvo do evento é um elemento HTMLScriptElement, HTMLImageElement, etc. Erros de recurso não têm a propriedade error preenchida.

window.addEventListener('error', (event) => {
  if (event.target) {
    // Erro de recurso: evento de carregamento falhou
    console.log('Erro de recurso:', event.target.src || event.target.href);
  } else {
    // Erro de execução
    console.log('Erro de execução:', event.message);
  }
});

Na prática, é comum combinar window.onerror com outras estratégias, como capturar erros em promises e erros assíncronos, que veremos a seguir.

unhandledrejection

Promises são amplamente utilizadas em JavaScript moderno para lidar com operações assíncronas. Quando uma promise é rejeitada e não há um catch ou segundo argumento para then, o erro pode passar despercebido. O evento unhandledrejection é disparado sempre que uma promise é rejeitada sem um tratador de rejeição. Isso permite capturar esses erros e tratá-los de forma centralizada.

O evento unhandledrejection fornece um objeto PromiseRejectionEvent com duas propriedades principais: promise (a promise que foi rejeitada) e reason (o valor da rejeição, geralmente um erro). É importante notar que esse evento não é disparado para rejeições que são tratadas posteriormente (por exemplo, se você adicionar um catch antes do final da microtask).

// Capturando rejeições não tratadas
window.addEventListener('unhandledrejection', (event) => {
  console.warn('Rejeição não tratada:', event.reason);
  // Enviar para monitoramento
  sendErrorToMonitoring(event.reason);
  event.preventDefault(); // evita que o erro seja logado no console como erro
});

// Simulando uma promise rejeitada sem catch
Promise.reject(new Error('Falha assíncrona'));

O método event.preventDefault() impede que o navegador exiba o erro no console (em alguns navegadores, isso pode não funcionar). É uma boa prática chamá-lo se você já está tratando o erro.

Uma nuance importante é que o evento unhandledrejection é disparado após a microtask, então se você adicionar um catch no próximo tick (por exemplo, usando setTimeout), o evento pode não ser disparado. Para evitar falsos positivos, você pode usar a propriedade event.promise para verificar se a promise ainda está pendente.

// Exemplo mais robusto
window.addEventListener('unhandledrejection', (event) => {
  // Verifica se a promise ainda não foi tratada
  if (event.promise && event.promise.status === 'pending') {
    console.log('Rejeição não tratada:', event.reason);
    event.preventDefault();
  }
});

Além de capturar rejeições não tratadas, você também pode usar o evento rejectionhandled, que é disparado quando uma rejeição que antes era não tratada recebe um tratador posteriormente. Isso é útil para rastrear erros que foram temporariamente não tratados.

window.addEventListener('rejectionhandled', (event) => {
  console.log('Rejeição tratada tardiamente:', event.reason);
});

Combinando window.onerror e unhandledrejection, você pode cobrir a maioria dos erros não capturados em sua aplicação. No entanto, é importante também capturar erros em chamadas assíncronas antigas, como callbacks, que não são cobertas por esses eventos. Nesse caso, você pode envolver suas funções com try/catch ou usar utilitários de captura.

Visão geral de ferramentas

Implementar um monitoramento manual é possível, mas em aplicações reais é mais eficiente usar ferramentas especializadas que oferecem coleta, agregação, alertas e análise de erros. Essas ferramentas geralmente fornecem SDKs que integram facilmente com seu código e capturam erros automaticamente, além de oferecerem recursos como mapas de origem (source maps) para depurar código minificado.

Algumas das ferramentas mais populares são:

  • Sentry: Uma das mais usadas, oferece rastreamento de erros em tempo real, suporte a várias linguagens e integração com frameworks. O SDK do Sentry para JavaScript captura exceções, promises rejeitadas e erros de recursos automaticamente.
  • LogRocket: Focada em gravação de sessões, permite ver o que o usuário fez antes do erro, incluindo logs, rede e estado da aplicação. Útil para reproduzir problemas.
  • Bugsnag: Oferece monitoramento de erros e estabilidade, com alertas e relatórios de impacto. Possui SDK para JavaScript e integrações com frameworks populares.
  • Rollbar: Similar ao Sentry, com foco em deploy e versionamento, permitindo associar erros a releases específicos.
  • TrackJS: Ferramenta leve, focada em capturar erros no navegador, com análise de pilha e contexto.

Para usar uma ferramenta como o Sentry, você normalmente instala o pacote via npm e inicializa com seu DSN (Data Source Name). Exemplo:

// Instalar: npm install @sentry/browser
import * as Sentry from "@sentry/browser";

Sentry.init({
  dsn: "https://seu-dsn@sentry.io/seu-projeto",
  environment: "production",
  release: "meu-projeto@1.0.0",
});

// A partir daqui, erros são capturados automaticamente
// Você também pode capturar manualmente:
try {
  // código que pode lançar erro
} catch (error) {
  Sentry.captureException(error);
}

Além de capturar erros, essas ferramentas geralmente permitem registrar eventos personalizados, breadcrumbs (passos do usuário) e até mesmo enviar informações de contexto, como usuário autenticado, para ajudar na depuração.

É importante lembrar que, ao usar um serviço de terceiros, você deve considerar a privacidade e a segurança dos dados. Evite enviar informações sensíveis, como senhas ou tokens, nos relatórios de erro. Configure a ferramenta para mascarar dados confidenciais.

Boas práticas e observações finais

Para um monitoramento eficaz, considere as seguintes práticas:

  • Capture erros tanto em window.onerror quanto em unhandledrejection para cobrir diferentes tipos de falhas.
  • Envie informações contextuais, como URL, user agent, estado da aplicação e stack trace, para facilitar a reprodução.
  • Use source maps em produção para que os stack traces apontem para o código original, não o minificado.
  • Defina alertas para erros críticos, mas evite sobrecarga de notificações.
  • Teste seu sistema de monitoramento em produção, simulando erros para garantir que os eventos estão sendo capturados corretamente.
  • Lembre-se de que nem todos os erros são bugs: erros de rede, por exemplo, podem ser temporários. Considere agrupar erros semelhantes.

Monitorar erros em produção é um processo contínuo. As ferramentas e técnicas apresentadas nesta aula fornecem a base para identificar e corrigir problemas antes que afetem muitos usuários.

Referências

Exercícios

  1. Implemente um monitoramento simples usando window.onerror que exiba a mensagem do erro e a linha em que ocorreu. Teste com um erro intencional.

    ✓ Resposta:
    window.onerror = function(message, source, lineno, colno, error) {
      console.log(`Erro: ${message} na linha ${lineno} do arquivo ${source}`);
    };
    throw new Error('Erro de teste');
  2. Crie um evento unhandledrejection que envie a razão da rejeição para um serviço de log fictício (apenas exiba no console). Teste com uma promise rejeitada.

    ✓ Resposta:
    window.addEventListener('unhandledrejection', (event) => {
      console.log('Rejeição não tratada:', event.reason);
      event.preventDefault();
    });
    Promise.reject(new Error('Falha na promise'));
  3. Diferencie erros de execução de erros de recurso no evento error. Crie um manipulador que logue o tipo de erro.

    ✓ Resposta:
    window.addEventListener('error', (event) => {
      if (event.target) {
        console.log('Erro de recurso:', event.target.src || event.target.href);
      } else {
        console.log('Erro de execução:', event.message);
      }
    });
    
    // Teste: adicione uma imagem que não existe no HTML
    // <img src="nao-existe.png">
  4. Explique por que window.onerror não captura erros de promises e como o unhandledrejection resolve isso. Dê um exemplo.

    ✓ Resposta:

    window.onerror é acionado apenas para exceções síncronas não tratadas. Erros em promises são tratados pelo mecanismo de microtasks, e rejeições não tratadas não são lançadas como exceções no escopo global. O evento unhandledrejection foi criado para capturar essas rejeições. Exemplo:

    // window.onerror não captura
    Promise.reject(new Error('Erro assíncrono'));
    
    // unhandledrejection captura
    window.addEventListener('unhandledrejection', (event) => console.log(event.reason));
  5. Pesquise sobre uma ferramenta de monitoramento (ex.: Sentry) e descreva como você integraria em um projeto JavaScript. Inclua o código de inicialização.

    ✓ Resposta:

    Exemplo com Sentry:

    // npm install @sentry/browser
    import * as Sentry from "@sentry/browser";
    
    Sentry.init({
      dsn: "https://example@sentry.io/12345",
      environment: "production",
      release: "v1.0.0",
    });
    
    // Erros não tratados são capturados automaticamente.
    // Para capturar manualmente:
    try {
      // código propenso a erro
    } catch (error) {
      Sentry.captureException(error);
    }