Why Handling Errors in JavaScript Is Harder Than You Think
Yacine Ouardi

You’d think error handling is simple, right? Throw a try-catch
here, maybe log an error there, and call it a day. But if you’ve worked on any decently sized frontend project, you know that’s just scratching the surface.
I’ve learned (sometimes the hard way) that dealing with errors in JavaScript is one of those things developers think they’re doing well… until things break in production. And trust me—they will break.
It’s Not Just Syntax—It’s Behavior
JavaScript’s quirks with errors are partly because it’s so flexible. For example:
javascript
try {
setTimeout(() => {
throw new Error('Oops');
}, 1000);
} catch (e) {
console.error('Caught:', e);
}
Would you expect the catch
to work here? Spoiler: it won’t. The error gets thrown asynchronously, and the surrounding try-catch
can’t catch it. I remember the first time I ran into this—it felt like JavaScript was trolling me.
And that’s just one example. Errors inside promises, event listeners, or async
functions behave differently from synchronous code. You can’t just slap a global handler and hope for the best.
React Makes It Even Trickier
If you’re using React (like I do in most projects), error boundaries are a lifesaver—but they have limits. They only catch errors during rendering, lifecycle methods, and constructors. Anything inside event handlers or async callbacks? You’re on your own.
jsx
class MyBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, info) {
console.log(error, info);
}
render() {
if (this.state.hasError) {
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
This pattern works for UI crashes, but runtime errors inside async operations won’t bubble here. You still need local handling or global event listeners (window.onerror
, unhandledrejection
).
Why It’s Still Hard—Even for Seniors
The complexity isn’t just technical—it’s also about knowing where errors can happen, how to surface them to users without freaking them out, and how to log them in ways that actually help you debug.
- Do you log everything? Or risk missing silent failures?
- Do you show users every error? Or quietly recover?
- Do you retry failed requests? Or fail fast?
Every choice has trade-offs. And in large teams, people have different assumptions about how errors should behave. That’s why I’ve seen senior devs spend hours (or days) tracing a bug that was swallowed somewhere deep in a promise chain.
What’s Helped Me So Far
Here are a few things that have saved me (or at least reduced my debugging rage):
✅ Always catch async/await errors explicitly:
javascript
async function fetchData() {
try {
const res = await fetch('/api/data');
const data = await res.json();
return data;
} catch (err) {
console.error('Fetch error:', err);
throw err; // or handle gracefully
}
}
✅ Use React Query, SWR, or similar libraries that handle loading/error states for you.
✅ Add a global error handler early in your app lifecycle to catch the weird edge cases:
javascript
window.addEventListener('error', (e) => {
console.error('Global error:', e);
});
window.addEventListener('unhandledrejection', (e) => {
console.error('Unhandled promise rejection:', e);
});
✅ Don’t just log to console—send errors to a tool like Sentry, Bugsnag, or even a custom endpoint.
Final Thoughts
If you’re feeling frustrated by error handling in JavaScript, you’re not alone. It’s genuinely tricky—and no single pattern works everywhere.
What’s helped me the most is treating error handling as a design challenge, not just a technical one. Think about what’s acceptable failure, what should crash loud, and what your users can recover from.
And expect to revisit your approach as your app grows. Because, like most things in software, it’s never “done”.