Frontend Error Tracking

Production engineering guide for frontend error tracking covering patterns, implementation strategies, and operational best practices.

Frontend error tracking captures, correlates, and surfaces runtime exceptions and user-facing issues in web applications. It matters when users report bugs that don’t appear in logs, when performance degrades without clear causes, and when production incidents require rapid diagnosis across multiple browser versions, devices, and network conditions. It is essential when deploying frequently, when frontend code runs in distributed environments, and when users interact with the application across diverse contexts.

Capturing Errors

Frontend error tracking begins with capturing exceptions and unhandled rejections from JavaScript. The core mechanism is a window.onerror handler, which is triggered on uncaught errors, including those from fetch, XMLHttpRequest, setTimeout, and Promise rejections.

window.addEventListener('error', (event) => {
  const { error, message, filename, lineno, colno } = event;
  
  fetch('/api/errors', {
    method: 'POST',
    body: JSON.stringify({
      message: message,
      stack: error?.stack,
      filename,
      line: lineno,
      column: colno,
      url: window.location.href,
      userAgent: navigator.userAgent,
      timestamp: Date.now(),
      user: { id: localStorage.getItem('userId') }
    }),
    headers: {
      'Content-Type': 'application/json',
      'X-Client-Version': 'v1.2.0'
    }
  });
});

This handler captures most runtime errors. For more precision, integrate window.addEventListener('unhandledrejection', ...) to capture rejected promises not handled with .catch().

window.addEventListener('unhandledrejection', (event) => {
  const { reason, promise } = event;
  const error = reason instanceof Error ? reason : new Error(String(reason));

  fetch('/api/errors', {
    method: 'POST',
    body: JSON.stringify({
      type: 'promise-rejection',
      message: error.message,
      stack: error.stack,
      url: window.location.href,
      timestamp: Date.now(),
      promiseId: promise._id, // custom tracking ID
      user: { id: localStorage.getItem('userId') }
    }),
    headers: { 'Content-Type': 'application/json' }
  });

  event.preventDefault(); // prevent default browser behavior
});

Errors are sent to a backend endpoint—typically /api/errors—which stores them in a database. The request body includes a minimal but rich set of context: URL, user agent, timestamp, and any serialized user state.

Aggregating and Correlating Errors

After capturing, errors must be aggregated and correlated. This involves grouping similar errors by signature and enriching them with context.

A common signature is a hash of message, stack, and filename. The backend computes this hash and groups errors with matching signatures into a single issue.

For example, a TypeError: Cannot read property 'name' of undefined on UserCard.jsx appears 127 times in the last hour. The backend returns a deduplicated list of errors, each with a unique errorId, signature, count, and firstSeen.

Correlation is achieved by attaching metadata to each error:

These fields are collected on every error event and used to filter and slice data.

{
  "errorId": "e1a3b8f2-9c4d-5e6f-7a8b-9c0d1e2f3a4b",
  "signature": "TypeError: Cannot read property 'name' of undefined in UserCard.jsx",
  "message": "TypeError: Cannot read property 'name' of undefined",
  "stack": "...\n    at UserCard.render (UserCard.jsx:42)\n    at render (index.js:15)",
  "filename": "UserCard.jsx",
  "url": "https://app.example.com/profile/123",
  "user": {
    "id": "u721",
    "session": "sess-9c3d2e1f"
  },
  "context": {
    "screenResolution": "1920x1080",
    "networkType": "4g",
    "deviceType": "mobile",
    "language": "en-US"
  },
  "timestamp": 1712345678900
}

Correlation extends beyond the initial error. When a user clicks a button that triggers a fetch, and that fetch fails with a 500 error, the backend correlates the initial TypeError with the network failure by matching sessionId and userId.

Reporting and Alerting

Reports surface errors in a dashboard, where developers can explore, filter, and triage.

A key feature is error timelines, which show when errors occurred relative to user actions. For example:

These events are linked through sessionId and userId, forming a narrative of the user’s session.

Alerts are triggered when error thresholds are breached. A rule like “more than 50 errors of type TypeError in 5 minutes” sends a notification via email, Slack, or Webhook.

{
  "alert": {
    "type": "threshold",
    "threshold": 50,
    "window": "5m",
    "errorType": "TypeError",
    "condition": "count > threshold",
    "triggeredAt": 1712345678900,
    "details": {
      "errorCount": 73,
      "signature": "TypeError: Cannot read property 'name' of undefined",
      "firstSeen": 1712345500000,
      "lastSeen": 1712345678900,
      "affectedUsers": 42,
      "recentEvents": [
        { "timestamp": 1712345678900, "message": "TypeError: Cannot read property 'name' of undefined", "url": "/profile/123" },
        { "timestamp": 1712345675600, "message": "Failed to fetch /api/users/123", "status": 500 }
      ]
    }
  }
}

Alerts are not just notifications. They link directly to the error dashboard, where engineers can drill down into the full context of a failure.

Debugging with Source Maps

Source maps are essential for meaningful stack traces. When a production build is minified, the stack trace is often unreadable. Source maps translate minified code back to original source.

The frontend application sends sourceMappingURL in the JavaScript file:

// app.js
function renderUser(user) {
  return user.name.toUpperCase() + ' (' + user.id + ')';
}

// sourceMappingURL: app.js.map

The backend receives the stack trace and uses the sourceMappingURL to fetch app.js.map and resolve each frame to the original source.

{
  "stack": [
    "TypeError: Cannot read property 'name' of undefined",
    "    at renderUser (app.js:12:15)",
    "    at UserCard.render (UserCard.jsx:42:10)",
    "    at render (index.js:15:5)"
  ]
}

The backend resolves app.js:12:15 to src/components/UserCard.jsx:23:17, allowing engineers to see the exact line of code that caused the error.

Source maps are not just a convenience. They are required for effective debugging. Without them, errors appear as cryptic lines in minified code, and engineers spend time reverse-engineering the source.

Handling Silent Failures

Silent failures—errors that do not raise exceptions but cause user-visible problems—are a common challenge. These include:

To capture these, the frontend includes a log function that sends structured logs to the backend:

function log(level, message, context = {}) {
  fetch('/api/logs', {
    method: 'POST',
    body: JSON.stringify({
      level,
      message,
      context,
      timestamp: Date.now(),
      url: window.location.href,
      user: { id: localStorage.getItem('userId') }
    }),
    headers: { 'Content-Type': 'application/json' }
  });
}

// Usage
log('warn', 'User image failed to load', { url: '/api/images/123' });
log('error', 'Failed to update user profile', { status: 500, endpoint: '/api/users/123' });

These logs appear alongside errors in the dashboard and can be filtered and joined with error events.

Failure Modes and Common Pitfalls

Several failure modes silently degrade the effectiveness of frontend error tracking:

A common mistake is sending error.message but not error.stack. This leads to shallow diagnostics: a TypeError appears, but the call stack is missing, making it hard to trace the origin.

Another pitfall is duplicate error reporting. When window.onerror and unhandledrejection both fire, and the backend does not de-duplicate by errorId, the same error appears twice.

Configuration and Best Practices

A standard configuration file for a frontend error tracking system might look like:

{
  "endpoint": "/api/errors",
  "batchInterval": 1000,
  "batchSize": 10,
  "enableSourceMaps": true,
  "includeUserContext": true,
  "includeNetworkInfo": true,
  "includeSessionId": true,
  "filterErrors": [
    {
      "type": "TypeError",
      "ignore": [
        "TypeError: Cannot read property 'length' of undefined",
        "TypeError: Cannot read property '0' of undefined"
      ],
      "threshold": 100
    },
    {
      "type": "PromiseRejection",
      "include": [
        "FetchError",
        "TimeoutError"
      ]
    }
  ],
  "headers": {
    "X-Client-Version": "1.2.0",
    "X-Environment": "production"
  }
}

This configuration enables batching, source maps, and user context. It also filters errors based on type and message, reducing noise.

Best practices include:

Frontend error tracking is not a one-off setup. It is a continuous process of capturing, correlating, reporting, and improving visibility into the user experience. Its success depends on precise, consistent, and automated error capture across the entire frontend lifecycle.

This page was rewritten on 18 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.