Error Boundaries & Error Handling Patterns
Master production-grade error handling in React. Learn Error Boundaries, error recovery strategies, error reporting with Sentry/LogRocket, graceful degradation, and how to build resilient applications that handle failures gracefully.
🛡️ 1. Error Boundary Pattern
💥 One uncaught error and BOOM — your entire React app goes white screen of death! 😱 Error Boundaries are your app's safety net, catching errors in the component tree and showing a friendly fallback instead of chaos.
🔧 The fix: Wrap risky components in class-based Error Boundaries (or use react-error-boundary) and you'll sleep better at night! 😴
🔴 Impact: CRITICAL — Without Error Boundaries, a single bug in one component can nuke your entire application!
📋 In this section: Error Boundary class component • getDerivedStateFromError • componentDidCatch logging • Fallback UI patterns
💡 Important: Error Boundaries only catch errors in:
• Component rendering
• Lifecycle methods
• Constructors
They do NOT catch errors in:
• Event handlers
• Async code (setTimeout, promises)
• Server-side rendering
• Errors thrown in the Error Boundary itself
// ❌ WRONG: No error handlingconst UserProfile = ({ userId }: { userId: string }) => {const user = getUserById(userId); // Throws if user not foundreturn (<div><h1>{user.name}</h1><p>{user.email}</p></div>);};// If getUserById throws, entire app crashes
// ✅ CORRECT: Error Boundary catches errorsimport React, { Component, ReactNode } from 'react';interface ErrorBoundaryState {hasError: boolean;error: Error | null;}interface ErrorBoundaryProps {children: ReactNode;fallback?: ReactNode;onError?: (error: Error, errorInfo: React.ErrorInfo) => void;}class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {constructor(props: ErrorBoundaryProps) {super(props);this.state = { hasError: false, error: null };}static getDerivedStateFromError(error: Error): ErrorBoundaryState {return { hasError: true, error };}componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {// Log error to error reporting servicethis.props.onError?.(error, errorInfo);console.error('Error caught by boundary:', error, errorInfo);}render() {if (this.state.hasError) {return this.props.fallback || (<div role="alert"><h2>Something went wrong</h2><p>{this.state.error?.message}</p><button onClick={() => this.setState({ hasError: false, error: null })}>Try again</button></div>);}return this.props.children;}}// Usage<ErrorBoundaryfallback={<ErrorFallback />}onError={(error, errorInfo) => {// Send to error reporting serviceerrorReportingService.captureException(error, { extra: errorInfo });}}><UserProfile userId={userId} /></ErrorBoundary>
🪝 2. Modern Error Boundary (Using react-error-boundary)
🎉 Writing class components in 2026? No thanks! The react-error-boundary library gives you all the error-catching goodness with a hooks-friendly API. It's like Error Boundaries got a modern makeover! 💅
🟠 Impact: HIGH — The go-to library for error boundaries in modern React — granular, composable, and hook-compatible!
📋 In this section: react-error-boundary setup • FallbackComponent pattern • Granular boundaries • resetKeys for auto-recovery
// ✅ Using react-error-boundaryimport { ErrorBoundary } from 'react-error-boundary';import { ErrorFallback } from './ErrorFallback';function ErrorFallback({ error, resetErrorBoundary }: { error: Error; resetErrorBoundary: () => void }) {return (<div role="alert" className="error-container"><h2>Something went wrong:</h2><pre style={{ color: 'red' }}>{error.message}</pre><button onClick={resetErrorBoundary}>Try again</button></div>);}// Usage with fallback<ErrorBoundaryFallbackComponent={ErrorFallback}onError={(error, errorInfo) => {// Log to error reporting servicelogErrorToService(error, errorInfo);}}onReset={() => {// Reset app state if neededclearAppState();}}><App /></ErrorBoundary>// ✅ Granular error boundariesfunction App() {return (<ErrorBoundary FallbackComponent={PageErrorFallback}><Header /><ErrorBoundary FallbackComponent={SidebarErrorFallback}><Sidebar /></ErrorBoundary><ErrorBoundary FallbackComponent={ContentErrorFallback}><MainContent /></ErrorBoundary><Footer /></ErrorBoundary>);}// ✅ Error boundary with retry logicfunction UserProfileWithRetry({ userId }: { userId: string }) {return (<ErrorBoundaryFallbackComponent={ErrorFallback}onResetKeys={[userId]} // Reset when userId changesresetKeys={[userId]}><UserProfile userId={userId} /></ErrorBoundary>);}
🌊 3. Async Error Handling
🚨 Plot twist: Error Boundaries don't catch async errors! 😤 That useEffect fetch that fails? That event handler that throws? They're on their own. Time to handle them explicitly with custom hooks and proper try/catch patterns! 🎣
🟠 Impact: HIGH — Most real-world errors are async — ignore them and your users see blank screens with zero feedback!
📋 In this section: useAsync custom hook • Cancellation with cleanup • Error state management • Error reporting integration
// ❌ WRONG: Unhandled async errorsconst UserProfile = ({ userId }: { userId: string }) => {const [user, setUser] = useState(null);useEffect(() => {// This error is NOT caught by Error BoundaryfetchUser(userId).then(setUser);}, [userId]);return <div>{user?.name}</div>;};
// ✅ CORRECT: Handle async errorsimport { useState, useEffect } from 'react';interface AsyncState<T> {data: T | null;error: Error | null;loading: boolean;}function useAsync<T>(asyncFn: () => Promise<T>,deps: React.DependencyList = []): AsyncState<T> {const [state, setState] = useState<AsyncState<T>>({data: null,error: null,loading: true,});useEffect(() => {let cancelled = false;async function execute() {try {setState(prev => ({ ...prev, loading: true, error: null }));const data = await asyncFn();if (!cancelled) {setState({ data, error: null, loading: false });}} catch (error) {if (!cancelled) {const err = error instanceof Error ? error : new Error(String(error));setState({ data: null, error: err, loading: false });// Log to error reporting serviceerrorReportingService.captureException(err);}}}execute();return () => {cancelled = true;};}, deps);return state;}// Usageconst UserProfile = ({ userId }: { userId: string }) => {const { data: user, error, loading } = useAsync(() => fetchUser(userId),[userId]);if (loading) return <Spinner />;if (error) return <ErrorDisplay error={error} />;if (!user) return <div>User not found</div>;return <div>{user.name}</div>;};
📊 4. Error Reporting (Sentry, LogRocket)
🔍 An error happened in production and nobody knows about it? That's the real nightmare! 👻 Plug in Sentry or LogRocket and you'll know about bugs before your users even finish complaining. Track, analyze, and squash those production bugs like a pro! 🐛🔨
🔴 Impact: CRITICAL — Flying blind in production without error reporting is a recipe for disaster!
📋 In this section: Sentry initialization • User context & breadcrumbs • LogRocket session replay • Error filtering
// ✅ Sentry Integrationimport * as Sentry from '@sentry/react';import { BrowserTracing } from '@sentry/tracing';// Initialize SentrySentry.init({dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,integrations: [new BrowserTracing(),],tracesSampleRate: 1.0,environment: process.env.NODE_ENV,beforeSend(event, hint) {// Filter out known errorsif (event.exception) {const error = hint.originalException;if (error instanceof ChunkLoadError) {// Ignore chunk load errors (network issues)return null;}}return event;},});// Wrap app with Sentry Error Boundaryimport { ErrorBoundary } from '@sentry/react';function App() {return (<ErrorBoundaryfallback={({ error, resetError }) => (<ErrorFallback error={error} resetError={resetError} />)}showDialog><YourApp /></ErrorBoundary>);}// ✅ Manual error reportingfunction handleAsyncError(error: Error, context?: Record<string, any>) {Sentry.captureException(error, {tags: {section: 'user-profile',},extra: context,level: 'error',});}// ✅ User contextSentry.setUser({id: user.id,email: user.email,username: user.name,});// ✅ Breadcrumbs for debuggingSentry.addBreadcrumb({category: 'navigation',message: 'User navigated to profile',level: 'info',});// ✅ LogRocket Integrationimport LogRocket from 'logrocket';LogRocket.init(process.env.NEXT_PUBLIC_LOGROCKET_APP_ID);// Identify userLogRocket.identify(user.id, {name: user.name,email: user.email,});// Capture exceptionsLogRocket.captureException(error, {tags: {userId: user.id,feature: 'checkout',},});
🔄 5. Error Recovery Strategies
💪 Errors will happen — it's how you bounce back that matters! Retry with exponential backoff, fall back to cached data, go offline gracefully, and degrade features instead of crashing. Your app should be a phoenix, not a house of cards! 🏗️🔥
🟠 Impact: HIGH — Resilient apps keep users happy even when things go wrong behind the scenes!
📋 In this section: Exponential backoff retry • Fallback cached data • Offline mode with localStorage • Graceful degradation
// ✅ Retry logic with exponential backoffasync function fetchWithRetry<T>(url: string,options: RequestInit = {},maxRetries = 3): Promise<T> {let lastError: Error | null = null;for (let attempt = 0; attempt <= maxRetries; attempt++) {try {const response = await fetch(url, options);if (!response.ok) {throw new Error(`HTTP ${response.status}: ${response.statusText}`);}return await response.json();} catch (error) {lastError = error instanceof Error ? error : new Error(String(error));if (attempt < maxRetries) {// Exponential backoff: 1s, 2s, 4sconst delay = Math.pow(2, attempt) * 1000;await new Promise(resolve => setTimeout(resolve, delay));continue;}}}throw lastError || new Error('Failed to fetch');}// ✅ Fallback datafunction useUserWithFallback(userId: string) {const { data, error, loading } = useAsync(() => fetchUser(userId), [userId]);// Fallback to cached data if fetch failsconst cachedUser = useCachedUser(userId);const user = data || cachedUser;return { user, error, loading };}// ✅ Offline modefunction useOfflineData<T>(fetchFn: () => Promise<T>, cacheKey: string) {const [data, setData] = useState<T | null>(() => {// Try to load from cache firstconst cached = localStorage.getItem(cacheKey);return cached ? JSON.parse(cached) : null;});useEffect(() => {async function fetchData() {try {const result = await fetchFn();setData(result);localStorage.setItem(cacheKey, JSON.stringify(result));} catch (error) {// If offline, use cached dataif (!navigator.onLine && data) {console.warn('Using cached data (offline)');return;}throw error;}}fetchData();}, []);return data;}// ✅ Graceful degradationfunction FeatureWithFallback() {const { data, error } = useAsync(() => fetchFeatureData());if (error) {// Show simplified version instead of crashingreturn <SimplifiedFeature />;}return <FullFeature data={data} />;}
🗄️ 6. Error State Management
🎯 Scattered try/catch blocks everywhere? That's chaos! 🌪️ Centralize your error handling with a dedicated ErrorContext and custom hooks. One place to rule all errors, one place to toast them, one place to report them! 💍
🔵 Impact: MEDIUM — Centralized error management keeps your codebase clean and your error UX consistent!
📋 In this section: ErrorContext provider • useError hook • Toast notifications • Global error collection
// ✅ Global error contextimport { createContext, useContext, useState, ReactNode } from 'react';interface ErrorContextType {errors: Error[];addError: (error: Error) => void;removeError: (index: number) => void;clearErrors: () => void;}const ErrorContext = createContext<ErrorContextType | undefined>(undefined);export function ErrorProvider({ children }: { children: ReactNode }) {const [errors, setErrors] = useState<Error[]>([]);const addError = (error: Error) => {setErrors(prev => [...prev, error]);// Also report to error serviceerrorReportingService.captureException(error);};const removeError = (index: number) => {setErrors(prev => prev.filter((_, i) => i !== index));};const clearErrors = () => {setErrors([]);};return (<ErrorContext.Provider value={{ errors, addError, removeError, clearErrors }}>{children}</ErrorContext.Provider>);}export function useError() {const context = useContext(ErrorContext);if (!context) {throw new Error('useError must be used within ErrorProvider');}return context;}// ✅ Error toast notificationsfunction ErrorNotification() {const { errors, removeError } = useError();return (<div className="error-notifications">{errors.map((error, index) => (<div key={index} role="alert" className="error-toast"><span>{error.message}</span><button onClick={() => removeError(index)}>×</button></div>))}</div>);}// ✅ Usage in componentsfunction MyComponent() {const { addError } = useError();const handleAction = async () => {try {await performAction();} catch (error) {addError(error instanceof Error ? error : new Error(String(error)));}};return <button onClick={handleAction}>Perform Action</button>;}
💬 7. User-Friendly Error Messages
😱 "TypeError: Cannot read properties of undefined" — your users should NEVER see that! Transform scary technical gibberish into friendly, actionable messages. "Oops, something went wrong. Try refreshing!" is way better than a raw stack trace! 🤗
🔵 Impact: MEDIUM — Friendly error messages turn frustrated users into patient ones who actually retry!
📋 In this section: Error message mapping • HTTP status translation • Contextual messages • Technical detail toggles
// ✅ Error message mappingconst ERROR_MESSAGES: Record<string, string> = {'NETWORK_ERROR': 'Unable to connect. Please check your internet connection.','UNAUTHORIZED': 'Your session has expired. Please log in again.','NOT_FOUND': 'The requested resource could not be found.','RATE_LIMIT': 'Too many requests. Please try again in a moment.','SERVER_ERROR': 'Something went wrong on our end. We're working on it!','VALIDATION_ERROR': 'Please check your input and try again.',};function getUserFriendlyMessage(error: Error): string {// Check for known error codesif (error.message in ERROR_MESSAGES) {return ERROR_MESSAGES[error.message];}// Check for HTTP status codesconst statusMatch = error.message.match(/HTTP (\d+)/);if (statusMatch) {const status = statusMatch[1];switch (status) {case '404':return ERROR_MESSAGES.NOT_FOUND;case '401':case '403':return ERROR_MESSAGES.UNAUTHORIZED;case '429':return ERROR_MESSAGES.RATE_LIMIT;case '500':case '502':case '503':return ERROR_MESSAGES.SERVER_ERROR;}}// Default messagereturn 'Something unexpected happened. Please try again.';}// ✅ Error display componentfunction ErrorDisplay({ error }: { error: Error }) {const userMessage = getUserFriendlyMessage(error);const [showDetails, setShowDetails] = useState(false);return (<div role="alert" className="error-display"><div className="error-message"><p>{userMessage}</p><button onClick={() => setShowDetails(!showDetails)}>{showDetails ? 'Hide' : 'Show'} technical details</button></div>{showDetails && (<details className="error-details"><summary>Technical Details</summary><pre>{error.stack}</pre></details>)}</div>);}// ✅ Contextual error messagesfunction getContextualErrorMessage(error: Error, context: string): string {const baseMessage = getUserFriendlyMessage(error);switch (context) {case 'checkout':return 'Unable to process your order. Please verify your payment information.';case 'profile':return 'Unable to load your profile. Please refresh the page.';case 'search':return 'Search is temporarily unavailable. Please try again later.';default:return baseMessage;}}