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.
Run: Open broken.html to see error tracking in action.
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:
.catch() handlers on async operationsWithout 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.
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.
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.
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!
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.
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);
};
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.
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.
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) |
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.
window.addEventListener('error') catches both JS errors and resource failuresevent instanceof ErrorEvent — ErrorEvent = JS error, plain Event = resource failuretrue as the third argument to addEventListener)unhandledrejection catches forgotten .catch() handlers on Promises