Chrome Extension Crash Reporting — How to Monitor Errors and Fix Bugs Fast

10 min read

Chrome Extension Crash Reporting — How to Monitor Errors and Fix Bugs Fast

Introduction

Chrome extensions run in complex environments with multiple execution contexts: popup pages, background service workers, content scripts injected into web pages, and options pages. When an error occurs in any of these contexts, users rarely report it—unless you have a crash reporting system in place. Without proper error tracking, you’re essentially flying blind, unaware of the issues affecting your users.

Crash reporting helps you proactively identify and fix bugs before users abandon your extension. It provides visibility into runtime failures, uncaught exceptions, and unhandled promise rejections across all extension contexts. This guide covers implementing comprehensive crash reporting that captures errors from every part your extension.

Why Crash Reporting Matters for Extension Quality

Extensions face unique challenges that traditional web applications don’t encounter. Users install your extension across different Chrome versions, operating systems, and with various other extensions that might conflict with yours. Content scripts run in unpredictable web page environments, and service workers can terminate unexpectedly due to Chrome’s memory management.

A robust crash reporting system provides:

The Chrome Web Store’s review process now actively considers crash rates and error handling. Extensions with poor error management risk visibility penalties.

Setting Up Global Error Handlers

The foundation of crash reporting starts with global error handlers that catch uncaught exceptions and unhandled promise rejections. Each extension context requires its own setup.

Background Service Worker and Popup Errors

In your background script or popup, add these handlers at the very top of your entry file:

// background.js or popup script
window.onerror = function(message, source, lineno, colno, error) {
  const errorData = {
    message: String(message),
    source: source,
    lineno: lineno,
    colno: colno,
    stack: error?.stack,
    url: chrome.runtime?.getManifest()?.manifest_version,
    timestamp: new Date().toISOString()
  };
  
  // Send to your error reporting service
  reportError(errorData);
  return false; // Allow default error handling
};

window.onunhandledrejection = function(event) {
  const errorData = {
    message: event.reason?.message || String(event.reason),
    stack: event.reason?.stack,
    type: 'unhandledRejection',
    timestamp: new Date().toISOString()
  };
  
  reportError(errorData);
};

function reportError(errorData) {
  // Queue errors and send in batches or immediately
  console.error('[Crash Reporter]', errorData);
}

Content Script Error Handling

Content scripts run in the context of web pages, so you need to wrap your code in try-catch blocks and set up error handlers carefully:

// content-script.js
window.onerror = function(message, source, lineno, colno, error) {
  // Only report errors that are likely from our extension
  // Filter out page errors unless they're caused by our scripts
  const isExtensionError = source?.includes('chrome-extension://');
  
  if (isExtensionError) {
    chrome.runtime.sendMessage({
      type: 'CONTENT_SCRIPT_ERROR',
      payload: {
        message: String(message),
        source: source,
        lineno: lineno,
        colno: colno,
        stack: error?.stack,
        url: window.location.href,
        timestamp: new Date().toISOString()
      }
    });
  }
  return false;
};

Capturing Stack Traces from Content Scripts and Service Workers

Stack traces are invaluable for debugging. However, each extension context has different ways of capturing them.

Service Worker Stack Traces

Service workers in Manifest V3 can use the Error.prototype.stack property just like regular JavaScript. Chrome also provides the chrome.runtime.lastError that you should check after API calls:

// Service worker - check for runtime.lastError after every API call
chrome.storage.local.get(['key'], (result) => {
  if (chrome.runtime.lastError) {
    reportError({
      type: 'runtime.lastError',
      message: chrome.runtime.lastError.message,
      context: 'storage.local.get',
      stack: new Error().stack
    });
  }
});

Content Script Stack Traces from Injected Scripts

If your content script injects additional scripts, wrap them in error handlers:

function injectScript(scriptContent) {
  const script = document.createElement('script');
  script.textContent = `
    (function() {
      try {
        ${scriptContent}
      } catch (e) {
        window.postMessage({
          type: 'EXTENSION_INJECTED_SCRIPT_ERROR',
          error: {
            message: e.message,
            stack: e.stack
          }
        }, '*');
      }
    })();
  `;
  document.documentElement.appendChild(script);
  script.remove();
}

// Listen for errors from injected scripts
window.addEventListener('message', (event) => {
  if (event.data?.type === 'EXTENSION_INJECTED_SCRIPT_ERROR') {
    reportError({
      type: 'injectedScriptError',
      ...event.data.error,
      url: window.location.href
    });
  }
});

Sending Error Reports to Your Backend

Rather than sending every error immediately (which can overwhelm your servers during error cascades), implement a queuing system that batches errors:

const errorQueue = [];
const MAX_QUEUE_SIZE = 10;
const FLUSH_INTERVAL = 5000; // 5 seconds

function reportError(errorData) {
  errorQueue.push({
    ...errorData,
    extensionVersion: chrome.runtime.getManifest().version,
    userAgent: navigator.userAgent
  });
  
  if (errorQueue.length >= MAX_QUEUE_SIZE) {
    flushErrors();
  }
}

async function flushErrors() {
  if (errorQueue.length === 0) return;
  
  const errorsToSend = [...errorQueue];
  errorQueue.length = 0;
  
  try {
    await fetch('https://your-api.com/crash-reports', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        errors: errorsToSend,
        metadata: {
          extensionId: chrome.runtime.id,
          version: chrome.runtime.getManifest().version,
          platform: navigator.platform
        }
      })
    });
  } catch (e) {
    // Re-queue failed errors (with limit to prevent infinite loops)
    if (errorQueue.length < 50) {
      errorQueue.push(...errorsToSend);
    }
  }
}

setInterval(flushErrors, FLUSH_INTERVAL);

Using Sentry or Similar Services with Extensions

Sentry provides official SDKs that work well with extensions. For Manifest V3 service workers and popup pages, you can use the JavaScript SDK:

import * as Sentry from '@sentry/browser';

Sentry.init({
  dsn: 'YOUR_SENTRY_DSN',
  release: chrome.runtime.getManifest().version,
  environment: process.env.NODE_ENV,
  
  // Filter out irrelevant errors
  ignoreErrors: [
    /Extension context invalidated/,
    /Message channel closed/,
    /Could not establish connection/
  ],
  
  // Capture unhandled promise rejections
  captureUnhandledRejections: true
});

// Add user context if available
Sentry.setUser({
  id: chrome.runtime.id
});

For content scripts, consider using a lightweight reporting approach that sends errors to the background script, which then forwards them to Sentry. This reduces the overhead in the page context:

// content-script - minimal error capture
window.onerror = function(...) {
  chrome.runtime.sendMessage({
    type: 'ERROR',
    payload: { message, stack, ... }
  });
  return false;
};
// background script - Sentry integration
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.type === 'ERROR') {
    Sentry.captureException(new Error(message.payload.message), {
      extra: message.payload,
      contexts: {
        browser: {
          url: sender.url,
          tabId: sender.tab?.id
        }
      }
    });
  }
});

Effective crash reporting isn’t just about collecting errors—it’s about using that data to drive improvements.

Key Metrics to Track

Prioritization Framework

Rank errors using this formula: Priority = (Error Rate) × (User Impact) × (Severity)

Creating Actionable Reports

Configure your error tracking tool to group similar errors automatically. Sentry’s fingerprinting helps:

Sentry.configureScope((scope) => {
  scope.setFingerprint([
    '{{ default }}',
    errorData.type,
    errorData.context
  ]);
});

Review crash reports weekly. Look for patterns: errors that spike after a new release indicate regression bugs. Errors in specific browser versions may reveal compatibility issues.

Implementation Checklist

Conclusion

Implementing comprehensive crash reporting across your Chrome extension’s various contexts gives you visibility into real-world issues affecting your users. Start with basic error handlers, integrate a service like Sentry for richer diagnostics, and establish regular review processes to turn crash data into actionable fixes. The investment pays dividends through improved user satisfaction, better reviews, and a more stable extension.

No previous article
No next article