The Cost of Silent Failures
The worst bugs aren't errors — they're silent failures. Code that fails without throwing, returns undefined instead of data, or catches errors and does nothing.
// TERRIBLE: error is swallowed, data is undefined, page shows blank
try {
const data = await fetch('/api/data').then(r => r.json());
setState(data);
} catch (e) {
// empty catch — the worst anti-pattern
}
// BETTER: log, show user-facing error, report to monitoring
try {
const data = await fetch('/api/data').then(r => r.json());
setState(data);
} catch (error) {
console.error('Data fetch failed:', error);
setError('Unable to load data. Please try again.');
reportError(error); // Send to Sentry/LogRocket/etc.
}
Every production app should follow this rule: if you catch it, handle it. If you can't handle it, let it propagate.
Custom Error Classes
Built-in Error gives you a message and stack trace but no context. Create custom errors:
class AppError extends Error {
constructor(message, statusCode = 500, code = 'INTERNAL_ERROR') {
super(message);
this.name = 'AppError';
this.statusCode = statusCode;
this.code = code;
}
}
class ValidationError extends AppError {
constructor(message, fields = {}) {
super(message, 400, 'VALIDATION_ERROR');
this.name = 'ValidationError';
this.fields = fields;
}
}
class NotFoundError extends AppError {
constructor(resource = 'Resource') {
super(${resource} not found, 404, 'NOT_FOUND');
this.name = 'NotFoundError';
}
}
// Usage
function getUser(id) {
const user = users.find(u => u.id === id);
if (!user) throw new NotFoundError('User');
return user;
}
function createUser(data) {
const errors = {};
if (!data.email) errors.email = 'Email is required';
if (!data.name) errors.name = 'Name is required';
if (Object.keys(errors).length > 0) {
throw new ValidationError('Invalid input', errors);
}
}
Custom errors let your error handler respond appropriately based on the error type.
Global Error Handling
Set up a safety net for unhandled errors:
Browser:// Catch unhandled errors
window.addEventListener('error', (event) => {
console.error('Unhandled error:', event.error);
reportError(event.error);
});
// Catch unhandled Promise rejections
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled rejection:', event.reason);
reportError(event.reason);
event.preventDefault(); // Prevent default console error
});
Node.js:
process.on('uncaughtException', (error) => {
console.error('Uncaught exception:', error);
reportError(error);
process.exit(1); // Exit — the process is in an unknown state
});
process.on('unhandledRejection', (reason) => {
console.error('Unhandled rejection:', reason);
reportError(reason);
});
These are safety nets, not primary error handling. Every async operation should have its own error handling.
Error Boundaries in React
React error boundaries catch rendering errors that try/catch can't:
"use client";
import { Component, type ReactNode } from 'react';
interface Props {
children: ReactNode;
fallback?: ReactNode;
}
interface State {
hasError: boolean;
error: Error | null;
}
class ErrorBoundary extends Component {
state: State = { hasError: false, error: null };
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
componentDidCatch(error: Error, info: React.ErrorInfo) {
console.error('Component error:', error, info.componentStack);
}
render() {
if (this.state.hasError) {
return this.props.fallback || (
Something went wrong
Try again
);
}
return this.props.children;
}
}
Wrap sections of your app that might fail:
Failed to load widget}>
In Next.js App Router, you can also use error.tsx files for route-level error handling.
Production Error Reporting
In production, errors need to go somewhere you can see them:
function reportError(error, context = {}) {
// 1. Always log locally
console.error('[Error]', error.message, context);
// 2. Send to your error tracking service
if (typeof window !== 'undefined') {
// Browser: send to backend
navigator.sendBeacon('/api/errors', JSON.stringify({
message: error.message,
stack: error.stack,
url: window.location.href,
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent,
...context,
}));
}
}
navigator.sendBeacon is ideal for error reporting because it sends data even if the page is closing — unlike fetch which gets cancelled on navigation.
For production apps, use a dedicated service like Sentry, LogRocket, or Betterstack. They provide stack traces, user session replay, error grouping, and alerting.