Frontend Error Handling: Catching What try/catch Misses
Why Sentry and Bugsnag catch more JavaScript errors than custom try/catch logic, and how to close the gap with global handlers and rejection listeners.
· 3 min read
Plenty of teams wire up custom error handling, add Sentry or Bugsnag on top “just in case,” and then notice the monitoring tool is catching errors their own code never saw. The gap isn’t sophistication, it’s coverage: what these tools hook into that a typical try...catch doesn’t.
Why Sentry and Bugsnag catch more
- Global error listeners. They hook
window.onerror(fires on any uncaught error, with message, source, line, column) andwindow.onunhandledrejection(fires on a rejected promise nobody handled). - Early, resilient initialization. They load before most of your own scripts and are built not to fail themselves.
- Async coverage. Errors thrown inside
setTimeout,setInterval,requestAnimationFrame, or an uncaught promise slip past a localtry...catchentirely. These tools specifically target that gap.
Building the same coverage yourself
Local try...catch for synchronous code. First line of defense, for errors you can predict and want to handle at the point of failure:
function processUserData(userData) {
try {
const parsedData = JSON.parse(userData);
console.log('User data processed:', parsedData);
} catch (error) {
console.error('Error parsing user data:', error);
alert('Failed to process user data. Please check your input.');
logToCustomErrorService(error, 'User Data Parsing');
}
}
.catch() on every promise. Unhandled rejections are the most common way errors go missing:
async function fetchData(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('Error fetching data:', error);
alert('Failed to fetch data. Please try again later.');
logToCustomErrorService(error, 'Data Fetching');
throw error; // re-throw so upstream handlers can still catch it
}
}
window.onerror as the safety net for whatever slips past local handling:
window.onerror = function(message, source, lineno, colno, error) {
console.error('Global uncaught error:', { message, source, lineno, colno, error });
logToCustomErrorService(error || new Error(message), 'Global Uncaught Error', {
source, lineno, colno
});
return true; // suppress the browser's default error message
};
The error argument can be undefined in older browsers or for certain error types: fall back to constructing an Error from message when it’s missing.
window.onunhandledrejection for promises nobody called .catch() on:
window.onunhandledrejection = function(event) {
console.error('Global unhandled promise rejection:', event.reason);
logToCustomErrorService(event.reason, 'Unhandled Promise Rejection');
};
One centralized logging function, so every path (local, global, third-party) reports through the same place with the same shape:
function logToCustomErrorService(error, context = 'General', extraInfo = {}) {
const errorDetails = {
message: error.message || 'Unknown error',
stack: error.stack || 'No stack trace available',
name: error.name || 'Error',
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent,
url: window.location.href,
context,
...extraInfo
};
// fetch('/api/log-error', {
// method: 'POST',
// headers: { 'Content-Type': 'application/json' },
// body: JSON.stringify(errorDetails)
// }).catch(console.error);
}
What actually matters here
Local handling is for errors you can meaningfully recover from or explain to the user. Global handlers are the backstop for everything you didn’t anticipate, not the primary mechanism. An empty catch block is worse than no catch block at all, it hides the failure instead of surfacing it. React’s error boundaries and Vue 3’s errorHandler cover the component-tree case and slot in alongside the global handlers described here.
Third-party monitoring is convenient, but it’s the same four mechanisms above wired up for you. Understanding them means you can decide, with intent, which errors you own end-to-end and which ones you’re content to let Sentry catch.