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;}}