When a JavaScript error fires in your browser and there's no try/catch around it, it disappears into the void — unless you've set up global error handlers to catch it. Most developers assume there's one way to handle these errors at runtime, but the truth is more nuanced: window.onerror, addEventListener('error'), and unhandledrejection each listen to different error types and execute at different moments. The difference between "errors my users report" and "every error my app throws" often comes down to which handlers you've wired up, and in what order.
This guide walks through the three pillars of global JavaScript error handling, what each catches and misses, and how to use them together to capture every uncaught exception in the browser before it silently breaks your app.
window.onerror: The foundation
The oldest and most widely supported global error handler is window.onerror. It fires whenever a synchronous script error or a runtime exception is thrown at the global scope — for example, calling a missing function or accessing a property on undefined.
window.onerror = (message, source, lineno, colno, error) => {
console.log(`Error: ${message} at ${source}:${lineno}:${colno}`);
// Send to your error tracker here
return true; // Returning true prevents default error handling
};
The five parameters tell you everything about the error: a human-readable message, the file it came from, the line and column numbers, and critically, the Error object itself with the stack trace. However, window.onerror only fires for synchronous errors. If you throw an error inside an async callback or a Promise, it won't catch it.
Also, returning true from window.onerror suppresses the browser's default error handling (the red error logged to the console). In production, you usually want to return true so error noise doesn't clutter logs, but during development it's often better to return false and see the details.
window.onerror is the most widely supported global handler across older browsers. If you're supporting IE11 or older Android browsers, it's your main option. Modern browsers support all three handlers, but combining them gives you the best coverage.
addEventListener('error'): Catching script loading and cross-origin errors
A second, often-overlooked global handler is the error event listener. It catches more than window.onerror does — specifically, errors that fire during script loading and resource fetch failures.
window.addEventListener('error', (event) => {
if (event instanceof ErrorEvent) {
// Synchronous error (same as window.onerror)
console.log(`Error: ${event.message}`);
} else {
// Resource loading error (failed script, image, etc.)
console.log(`Resource failed to load: ${event.target.src}`);
}
});
The key difference: when a script file fails to load (network error, 404, CORS rejection), addEventListener('error') fires but window.onerror does not. For most applications, this matters less often, but in high-traffic production environments with flaky CDNs or third-party scripts, capturing these failures can be the difference between spotting a deployment problem immediately and spending an hour debugging why some users see a broken UI.
One trap: cross-origin script errors (scripts loaded from a different domain) are obscured by the browser's CORS policy — you'll see only "Script error." with no line number. To reveal the actual error, the remote server must send the Access-Control-Allow-Origin header, and the <script> tag must have crossorigin="anonymous". See fixing 'Script error.' for a deeper dive.
unhandledrejection: The Promise handler
Modern JavaScript runs on Promises, and a rejected Promise that has no .catch() handler or try/catch in an async function is an unhandled rejection. These errors are invisible to window.onerror and addEventListener('error') — you need a third handler.
window.addEventListener('unhandledrejection', (event) => {
console.log(`Unhandled rejection: ${event.reason}`);
// Prevent the browser's default error logging
event.preventDefault();
});
This handler fires before the browser logs "Uncaught (in promise)" to the console. The event.reason is either the Error object (if the Promise was rejected with an error) or any value (if it was rejected with a non-Error, like a string or object).
Unhandled rejections are common in real applications. A failed API fetch, a race condition, or a library that rejects a Promise can all trigger this handler. Without it, the error vanishes from your logs and your error tracker entirely.
Attach all three handlers at the very top of your app initialization, before any other scripts run. If you wait until after user interactions or library initialization, you might miss early errors. Most Sentry SDKs do this automatically, but if you're building a custom handler, prioritize early initialization.
Putting it all together: A production-ready global handler
A real error tracker needs to listen to all three channels simultaneously. Here's a realistic setup:
function captureGlobalErrors(onError) {
// Sync errors and runtime exceptions
window.onerror = (message, source, lineno, colno, error) => {
onError({
type: 'error',
message,
source,
lineno,
colno,
stack: error?.stack,
});
return true;
};
// Resource loading errors and cross-origin script errors
window.addEventListener('error', (event) => {
if (event instanceof ErrorEvent) {
// Handled by window.onerror above; skip duplicate
return;
}
onError({
type: 'resource',
message: `Failed to load ${event.target.tagName}: ${event.target.src}`,
stack: null,
});
});
// Unhandled Promise rejections
window.addEventListener('unhandledrejection', (event) => {
onError({
type: 'unhandledRejection',
message: typeof event.reason === 'string'
? event.reason
: event.reason?.message || 'Unknown rejection',
stack: event.reason?.stack,
});
event.preventDefault();
});
}
// Initialize your error tracking
captureGlobalErrors((errorInfo) => {
// Send to LightTrace or your error tracker
fetch('https://<key>@your-lighttrace-host/1/store/', {
method: 'POST',
body: JSON.stringify(errorInfo),
}).catch(() => {
// Silently fail if the tracker is unreachable
});
});
In practice, you'll rarely write this yourself — the Sentry browser SDK and other error trackers handle it for you, installing all three handlers in the right order and deduplicating events so you don't get two reports for the same error.
What these handlers miss
Even with all three in place, some errors slip through. Browser extensions can swallow errors. Some modern errors (like those inside Web Workers or within an iframe's sandbox) may not propagate to the parent window. Errors inside async callbacks that execute much later (seconds or minutes after the initial action) can feel orphaned from their context.
This is why global error handlers are necessary but not sufficient: they need breadcrumbs to record user actions, clicks, and requests leading up to the crash. They need source maps to de-minify stack traces so you can read them. And they need automatic grouping to collapse identical errors into a single issue so you're not drowning in noise.
See JavaScript error monitoring for a complete picture of what modern browser error tracking looks like, or best JavaScript error tracking tools to compare platforms that automate this setup for you.
Starting with an SDK
If you're instrumenting a new app, don't build custom global handlers from scratch. Use the Sentry SDK or another established error tracker that already bundles all three handlers, adds breadcrumbs automatically, and integrates with your framework.
Custom error handlers leak context and scale poorly. If you're shipping production code, use an SDK that handles fingerprinting, deduplication, and alerting. Manually logging errors to a backend is the path to alert fatigue and silent failures.
The Sentry SDK for browsers installs all three handlers for you and uses error grouping to turn raw events into actionable issues. Because LightTrace is fully Sentry-SDK-compatible, you can point the standard @sentry/browser package at LightTrace with just a DSN change — no code rewrite needed.
Start tracking errors in minutes
Install the Sentry browser SDK, point it at LightTrace, and start capturing every uncaught error in your browser — free up to 5,000 events a month.
Global error handling is the foundation of any production JavaScript app. Wire up all three handlers, add an error tracker to turn raw events into grouped issues, and you'll move from hoping your app doesn't break to knowing the moment it does.