Module 07: Error Tracking

Users rarely report JavaScript errors. They just leave. Your analytics data shows a bounce, but not why. Error tracking closes that gap — it captures JS runtime errors, unhandled promise rejections, and resource load failures automatically, without any user action required. This is the most operationally valuable analytics you can collect.

Demo Files

Run: Open broken.html to see error tracking in action.

Why Track Errors?

Users rarely report JavaScript errors. They just leave. A broken checkout button, a failed API call, a missing image — any of these can silently kill conversions. Error tracking captures what users never tell you:

Without error tracking, you are flying blind. Your server logs show 200 OK responses, your synthetic tests pass, but real users on real networks with real browser extensions are hitting errors you never see. Error tracking is the highest-value analytics you can add — it catches what users never report.

Types of Browser Errors

1. JavaScript Runtime Errors

The window.addEventListener('error') handler fires for both JavaScript errors and resource load failures. You distinguish them by checking the event type: an ErrorEvent is a JavaScript error, while a plain Event is a resource failure.

window.addEventListener('error', (event) => {
  // event is an ErrorEvent when it's a JS error
  if (event instanceof ErrorEvent) {
    reportError({
      type: 'js-error',
      message: event.message,
      source: event.filename,
      line: event.lineno,
      column: event.colno,
      stack: event.error ? event.error.stack : '',
      url: window.location.href
    });
  }
});

The ErrorEvent object gives you everything you need to diagnose the problem: the error message, the file and line number where it occurred, and a stack trace. The event.error property is the actual Error object thrown, which contains the .stack property — but note that event.error can be null for cross-origin script errors (due to the same-origin policy), so always check before accessing .stack.

2. Unhandled Promise Rejections

Modern JavaScript uses Promises and async/await heavily. A forgotten .catch() or a failed await without try/catch produces an unhandled rejection. These are invisible to users but indicate real bugs — a failed API call, a network timeout, or a logic error in asynchronous code.

window.addEventListener('unhandledrejection', (event) => {
  const reason = event.reason;
  reportError({
    type: 'promise-rejection',
    message: reason instanceof Error ? reason.message : String(reason),
    stack: reason instanceof Error ? reason.stack : '',
    url: window.location.href
  });
});

The event.reason can be anything — an Error object, a string, or even undefined. Production code should handle all cases. When reason is an Error, you get a message and stack trace. When it is a string or other value, you convert it to a string for the error report.

3. Resource Load Failures

When an image fails to load, a script 404s, or a stylesheet is unreachable, the browser fires an error event on the element that failed. These are not ErrorEvent instances — they are plain Event objects.

window.addEventListener('error', (event) => {
  // Resource errors bubble up as plain Events (not ErrorEvent)
  if (!(event instanceof ErrorEvent)) {
    const target = event.target;
    if (target && (target.tagName === 'IMG' || target.tagName === 'SCRIPT' || target.tagName === 'LINK')) {
      reportError({
        type: 'resource-error',
        tagName: target.tagName,
        src: target.src || target.href || '',
        url: window.location.href
      });
    }
  }
}, true); // Note: must use capture phase!
Warning: Resource load errors do NOT bubble in the DOM — they only fire on the element itself. To catch them globally, you MUST use the capture phase by passing true as the third argument to addEventListener. Without true, the window handler will never see resource errors.

We check target.tagName to filter for meaningful resource types: images (IMG), scripts (SCRIPT), and stylesheets (LINK). The source URL comes from target.src for images and scripts, or target.href for link elements.

4. Wrapping console.error (Optional)

Some libraries and frameworks log errors to console.error instead of throwing them. You can intercept these by wrapping the native console.error function:

const originalConsoleError = console.error;
console.error = (...args) => {
  reportError({
    type: 'console-error',
    message: args.map(String).join(' '),
    url: window.location.href
  });
  originalConsoleError(...args);
};
Warning: Wrapping console.error is aggressive — it intercepts errors logged by third-party scripts too. Use sparingly and consider whether you want that level of noise. In our collector, we do not wrap console.error by default — it is shown here as an optional technique.

Error Deduplication

The same error can fire hundreds of times — for example, an error inside a requestAnimationFrame loop or a render cycle will fire on every frame. Without deduplication, a single bug can produce a beacon storm that overwhelms your endpoint and inflates your data.

Deduplication uses a Set to track which errors have already been reported, combined with a hard cap on the total number of error beacons per page load:

const reportedErrors = new Set();
let errorCount = 0;
const MAX_ERRORS = 10;

function reportError(errorData) {
  // Rate limit
  if (errorCount >= MAX_ERRORS) return;

  // Deduplicate by message + source + line
  const key = `${errorData.type}:${errorData.message}:${errorData.source || ''}:${errorData.line || ''}`;
  if (reportedErrors.has(key)) return;
  reportedErrors.add(key);
  errorCount++;

  // Send error beacon
  send({
    type: 'error',
    error: errorData,
    timestamp: new Date().toISOString(),
    url: window.location.href
  });
}

The deduplication key combines the error type, message, source file, and line number. Two errors with the same key are treated as the same bug. The MAX_ERRORS cap (10 per page load) prevents a catastrophic failure from flooding your analytics pipeline.

Error Payload Structure

Here is a sample error beacon payload showing a JavaScript runtime error:

{
  "type": "error",
  "error": {
    "type": "js-error",
    "message": "Cannot read properties of undefined (reading 'name')",
    "source": "https://example.com/app.js",
    "line": 42,
    "column": 15,
    "stack": "TypeError: Cannot read properties of undefined...\n    at getUser (app.js:42:15)\n    at renderProfile (app.js:78:3)"
  },
  "timestamp": "2026-01-15T08:30:00.000Z",
  "url": "https://example.com/profile"
}

The outer type field distinguishes error beacons from pageview beacons. The nested error object contains the error details. The error.type field tells you what kind of error it was: js-error, promise-rejection, resource-error, or console-error. Each type includes the fields relevant to that error category.

Error Type Key Fields When It Fires
js-error message, source, line, column, stack Uncaught exception in JS code
promise-rejection message, stack Promise rejected without a .catch()
resource-error tagName, src Image, script, or stylesheet fails to load
console-error message console.error() is called (if wrapped)

Cross-Reference

Cross-Reference: The reference collector.js has a minimal reportError() (line 514) that captures name, message, URL, and stack. Our version is more comprehensive: it distinguishes error types (JS, promise, resource), deduplicates, and rate-limits. See analytics-overview.html Section 10 (Error Tracking) for the conceptual foundations.

Summary