
Most error boundary examples are useless - they log "Error occurred" and show "Something went wrong." That doesn't help you fix anything at 3am when users are screaming about broken checkout flows.
This is my current setup after debugging too many production fires:
class ProductionErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false, errorInfo: null, errorId: null };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// Generate error ID so users can reference it in bug reports
const errorId = 'ERR_' + Date.now().toString(36);
const errorData = {
errorId,
error: error.message,
stack: error.stack,
componentStack: errorInfo.componentStack,
url: window.location.href,
userAgent: navigator.userAgent,
timestamp: new Date().toISOString(),
userId: this.props.userId,
buildVersion: process.env.REACT_APP_VERSION,
// Memory usage - crucial for mobile debugging
memoryUsage: performance.memory ? {
used: Math.round(performance.memory.usedJSHeapSize / 1048576) + 'MB',
total: Math.round(performance.memory.totalJSHeapSize / 1048576) + 'MB',
limit: Math.round(performance.memory.jsHeapSizeLimit / 1048576) + 'MB'
} : 'not available',
viewport: `${window.innerWidth}x${window.innerHeight}`,
// Network status - helps debug offline issues
online: navigator.onLine,
connectionType: navigator.connection?.effectiveType || 'unknown'
};
console.error('Production Error:', errorData);
// Send to your error tracking service
if (window.Sentry) {
window.Sentry.captureException(error, {
tags: { errorId },
extra: errorData
});
}
this.setState({ errorId });
}
render() {
if (this.state.hasError) {
return (
<div className="error-fallback">
<h2>Something broke</h2>
<p>Error ID: <code>{this.state.errorId}</code></p>
<p>This error has been logged. If it keeps happening, report this ID.</p>
<button onClick={() => window.location.reload()}>
Reload page
</button>
<button
onClick={() => this.setState({ hasError: false, errorId: null })}
style={{ marginLeft: '10px' }}
>
Try again
</button>
{process.env.NODE_ENV === 'development' && (
<details style={{ marginTop: '20px' }}>
<summary>Error details (dev only)</summary>
<pre style={{ fontSize: '12px', marginTop: '10px' }}>
{this.state.errorInfo?.componentStack}
</pre>
</details>
)}
</div>
);
}
return this.props.children;
}
}

Catching All the Async Errors Error Boundaries Miss
Error boundaries can't catch async errors. You need global handlers to catch the API timeouts and Promise rejections that kill your app:
// This catches Promise rejections that don't have .catch() handlers
// Basically all the async stuff that kills your app silently
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled promise rejection:', event.reason);
// Try to figure out what broke
const errorData = {
type: 'unhandledrejection',
reason: event.reason?.message || event.reason?.toString() || 'Unknown error',
stack: event.reason?.stack || 'No stack trace available',
url: window.location.href,
timestamp: new Date().toISOString(),
// Pattern matching for common failures
likelySource:
event.reason?.message?.includes('fetch') ? 'API call failed' :
event.reason?.message?.includes('timeout') ? 'Network timeout' :
event.reason?.message?.includes('404') ? 'Resource not found' :
event.reason?.message?.includes('500') ? 'Server error' : 'Unknown'
};
// Send to monitoring (this saved my ass many times)
if (window.Sentry) {
window.Sentry.captureException(event.reason, {
tags: { errorType: 'async_failure' },
extra: errorData
});
}
// Prevent the browser from logging to console (optional)
// event.preventDefault();
});
// Catches everything else that escapes - syntax errors, reference errors, etc.
window.addEventListener('error', (event) => {
console.error('Global JavaScript error:', event.error);
const errorData = {
type: 'javascript_error',
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
stack: event.error?.stack || 'No stack trace',
timestamp: new Date().toISOString(),
url: window.location.href
};
// Log it somewhere useful
if (window.Sentry) {
window.Sentry.captureException(event.error || new Error(event.message));
}
});

Adding Context That Actually Helps Debug Problems
Generic error messages are useless. You need context: what was the user doing when it broke? What API calls failed? What's the component state?
// Hook for tracking context that helps debug production issues
function useErrorContext() {
const [context, setContext] = useState({});
const logError = (error, additionalContext = {}) => {
const errorId = 'ERR_' + Date.now().toString(36);
const errorData = {
errorId,
error: error.message,
stack: error.stack,
context: { ...context, ...additionalContext },
url: window.location.href,
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent,
viewport: `${window.innerWidth}x${window.innerHeight}`,
online: navigator.onLine
};
console.error('Context Error:', errorData);
if (window.Sentry) {
window.Sentry.captureException(error, {
tags: { errorId },
extra: errorData
});
}
};
return { setContext, logError };
}
// Real example: checkout component that kept failing in production
function CheckoutForm({ orderId, userId }) {
const { setContext, logError } = useErrorContext();
const [payment, setPayment] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Set context for any errors that happen in this component
setContext({
component: 'CheckoutForm',
orderId,
userId,
sessionStart: Date.now()
});
// Set up timeout before making API call
const timeoutId = setTimeout(() => {
logError(new Error('Payment API timeout - user probably on slow connection'), {
action: 'fetchPaymentMethods',
orderId,
timeoutAfter: '5000ms',
connectionType: navigator.connection?.effectiveType || 'unknown',
downlink: navigator.connection?.downlink || 'unknown'
});
setLoading(false);
}, 5000);
fetchPaymentMethods(orderId)
.then(methods => {
clearTimeout(timeoutId);
setPayment(methods);
setLoading(false);
})
.catch(error => {
clearTimeout(timeoutId);
logError(error, {
action: 'fetchPaymentMethods',
orderId,
httpStatus: error.status || 'network_error',
responseTime: Date.now() - Date.now(), // Would need proper timing
retryAttempt: 0
});
setLoading(false);
});
}, [orderId, userId]);
if (loading) return <div>Loading payment options...</div>;
if (!payment) return <div>Failed to load payment methods</div>;
return <div>Payment form here</div>;
}

Testing Error Boundaries Without Getting Burned in Production
Your error boundaries work fine in development, then production deploys and everything breaks. Production is a different beast:
- Minified code - Stack traces become "at Object.r (main.js:1:2847)" - completely useless
- Bad network conditions - API timeouts, packet loss, slow connections
- Memory limits - Your iPhone has 6GB RAM, users have 2GB Android phones
- Browser differences - Safari handles errors differently than Chrome
Test with production builds locally or you're flying blind:
npm run build
npx serve -s build
## Throttle network to "Slow 3G" in DevTools and watch your app die
Actually test on real mobile devices. Chrome DevTools device simulation is useful but doesn't catch memory pressure issues.
Memory debugging: Open DevTools Memory tab, force garbage collection, take heap snapshots. I've debugged error boundaries that leaked memory because they held references to massive error objects in state. Mobile users would get crashes after 10-15 error boundary activations.

Source Maps That Don't Completely Suck
Your source maps are probably broken. Test with this in your error boundary:
// Add to componentDidCatch to check if source maps work
console.log('Source map test:', new Error().stack);
If you see at Object.r (main.js:1:2847)
in production, your source maps are broken. Fix your webpack config:
// webpack.config.js
module.exports = {
devtool: process.env.NODE_ENV === 'production'
? 'hidden-source-map' // Don't expose source maps to users
: 'eval-source-map', // Fast rebuilds in dev
optimization: {
minimize: process.env.NODE_ENV === 'production',
minimizer: [
new TerserPlugin({
terserOptions: {
keep_fnames: false, // Keep function names for debugging
mangle: {
keep_fnames: false
},
},
}),
],
},
};
Upload source maps to your error service. Sentry CLI automates this:
sentry-cli releases files $RELEASE upload-sourcemaps ./build/static/js --strip-prefix build/static/js
Truth: Production error debugging is 90% proper logging setup, 10% fixing the actual bug. Get the logging infrastructure right first, then fix the bugs.
Resources That Don't Waste Your Time
Start with react-error-boundary - it's production-ready and handles most edge cases. If you need custom implementation, React's ComponentDidCatch API docs explain the behavior, and getDerivedStateFromError reference covers the lifecycle.
Browser differences will fuck you over. MDN's Error API docs explain what works where. Window error event handling and Promise rejection events behave differently across browsers.
Source map configuration is critical. Webpack devtool options explain the different map types. Create React App source maps are configured differently than custom webpack setups. Babel source map settings affect debugging quality.
For production deployment, Sentry's source map upload guide covers CI/CD integration. Vercel Next.js source maps and Netlify build configuration have platform-specific gotchas.
Testing error boundaries is often overlooked. Jest error boundary testing patterns show how to test componentDidCatch. React Testing Library error testing covers user-facing error scenarios. Cypress error handling testing helps with integration tests.
Performance monitoring integration is essential. Web Vitals monitoring tracks user experience impact. Performance Observer API provides runtime metrics. Real User Monitoring setup helps correlate errors with performance issues.