Error Boundaries & Suspense
Introduction to Error Handling in React
React provides powerful mechanisms for handling errors gracefully: Error Boundaries catch JavaScript errors in component trees, while Suspense manages asynchronous operations and loading states. Together, they create resilient user experiences.
Error Boundaries: Catching Component Errors
Error Boundaries are React components that catch JavaScript errors anywhere in their child component tree, log errors, and display a fallback UI. They work like JavaScript try/catch blocks but for components.
Important: Error Boundaries must be class components. Functional components cannot be Error Boundaries (yet), but they can be wrapped by them.
// src/components/ErrorBoundary.jsx
import React from 'react';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = {
hasError: false,
error: null,
errorInfo: null
};
}
// Called when an error is thrown during rendering
static getDerivedStateFromError(error) {
// Update state so the next render will show the fallback UI
return { hasError: true };
}
// Called after an error has been thrown
componentDidCatch(error, errorInfo) {
// Log error to error reporting service
console.error('Error caught by boundary:', error, errorInfo);
// You can log to services like Sentry, LogRocket, etc.
// logErrorToService(error, errorInfo);
this.setState({
error,
errorInfo
});
}
resetError = () => {
this.setState({
hasError: false,
error: null,
errorInfo: null
});
};
render() {
if (this.state.hasError) {
// Fallback UI
return (
<div style={{
padding: '2rem',
backgroundColor: '#fee',
border: '1px solid #fcc',
borderRadius: '0.5rem'
}}>
<h2>Something went wrong</h2>
<details style={{ whiteSpace: 'pre-wrap' }}>
<summary>Error Details</summary>
<p>{this.state.error && this.state.error.toString()}</p>
<p>{this.state.errorInfo && this.state.errorInfo.componentStack}</p>
</details>
<button onClick={this.resetError}>Try Again</button>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;
Using Error Boundaries
Wrap components that might throw errors with the Error Boundary:
import ErrorBoundary from './components/ErrorBoundary';
import UserProfile from './components/UserProfile';
import Dashboard from './components/Dashboard';
function App() {
return (
<div>
<h1>My App</h1>
{/* Wrap individual components */}
<ErrorBoundary>
<UserProfile userId={123} />
</ErrorBoundary>
{/* Or wrap entire sections */}
<ErrorBoundary>
<Dashboard>
<Stats />
<Charts />
<RecentActivity />
</Dashboard>
</ErrorBoundary>
</div>
);
}
// Component that might throw an error
function BuggyComponent({ user }) {
if (!user) {
throw new Error('User is required!');
}
return <div>{user.name}</div>;
}
Error Boundaries Do NOT Catch:
- Errors in event handlers (use try/catch)
- Asynchronous code (setTimeout, promises)
- Server-side rendering errors
- Errors thrown in the Error Boundary itself
Advanced Error Boundary with Logging
Create a production-ready Error Boundary with error tracking:
import React from 'react';
class ProductionErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = {
hasError: false,
error: null,
errorInfo: null,
eventId: null
};
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// Log to external service in production
if (process.env.NODE_ENV === 'production') {
// Example: Sentry integration
// const eventId = Sentry.captureException(error, {
// contexts: { react: { componentStack: errorInfo.componentStack } }
// });
// this.setState({ eventId });
// Or your custom logging service
fetch('/api/log-error', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
error: error.toString(),
componentStack: errorInfo.componentStack,
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent,
url: window.location.href
})
});
}
this.setState({ error, errorInfo });
}
render() {
if (this.state.hasError) {
return (
<div className="error-container">
<h2>Oops! Something went wrong</h2>
<p>We've been notified and are working on a fix.</p>
{process.env.NODE_ENV === 'development' && (
<details>
<summary>Error Details (Development Only)</summary>
<pre>{this.state.error.toString()}</pre>
<pre>{this.state.errorInfo.componentStack}</pre>
</details>
)}
<button onClick={() => window.location.reload()}>
Reload Page
</button>
</div>
);
}
return this.props.children;
}
}
Suspense: Managing Async Operations
Suspense lets components "wait" for something before rendering, showing fallback UI during loading:
import { Suspense, lazy } from 'react';
// Lazy load components
const HeavyComponent = lazy(() => import('./components/HeavyComponent'));
const Dashboard = lazy(() => import('./components/Dashboard'));
const UserProfile = lazy(() => import('./components/UserProfile'));
function App() {
return (
<div>
{/* Basic Suspense with loading fallback */}
<Suspense fallback={<div>Loading...</div>}>
<HeavyComponent />
</Suspense>
{/* Multiple components in one Suspense */}
<Suspense fallback={<LoadingSpinner />}>
<Dashboard />
<UserProfile userId={123} />
</Suspense>
{/* Nested Suspense boundaries */}
<Suspense fallback={<div>Loading page...</div>}>
<MainLayout>
<Suspense fallback={<div>Loading sidebar...</div>}>
<Sidebar />
</Suspense>
<Suspense fallback={<div>Loading content...</div>}>
<Content />
</Suspense>
</MainLayout>
</Suspense>
</div>
);
}
// Custom loading component
function LoadingSpinner() {
return (
<div style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '200px'
}}>
<div className="spinner">Loading...</div>
</div>
);
}
Combining Error Boundaries and Suspense
Use both together for robust error handling and loading states:
import { Suspense, lazy } from 'react';
import ErrorBoundary from './components/ErrorBoundary';
const AsyncComponent = lazy(() => import('./components/AsyncComponent'));
function App() {
return (
<ErrorBoundary>
<Suspense fallback={<LoadingFallback />}>
<AsyncComponent />
</Suspense>
</ErrorBoundary>
);
}
// Better: Separate loading and error states
function RobustAsyncLoader({ children }) {
return (
<ErrorBoundary fallback={<ErrorFallback />}>
<Suspense fallback={<LoadingFallback />}>
{children}
</Suspense>
</ErrorBoundary>
);
}
// Usage
function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
<RobustAsyncLoader>
<AsyncCharts />
</RobustAsyncLoader>
<RobustAsyncLoader>
<AsyncUserList />
</RobustAsyncLoader>
</div>
);
}
Skeleton Loading UI
Create better loading experiences with skeleton screens:
// Skeleton component for loading state
function UserCardSkeleton() {
return (
<div className="user-card skeleton">
<div className="skeleton-avatar"></div>
<div className="skeleton-text"></div>
<div className="skeleton-text short"></div>
</div>
);
}
/* Skeleton CSS */
.skeleton {
animation: pulse 1.5s ease-in-out infinite;
}
.skeleton-avatar {
width: 60px;
height: 60px;
border-radius: 50%;
background: #e0e0e0;
}
.skeleton-text {
height: 16px;
background: #e0e0e0;
border-radius: 4px;
margin: 8px 0;
}
.skeleton-text.short {
width: 60%;
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
// Usage with Suspense
function UserList() {
return (
<Suspense fallback={
<div>
<UserCardSkeleton />
<UserCardSkeleton />
<UserCardSkeleton />
</div>
}>
<AsyncUserList />
</Suspense>
);
}
Error Recovery Strategies
Implement different recovery strategies based on error types:
import React from 'react';
class SmartErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = {
hasError: false,
error: null,
retryCount: 0
};
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
console.error('Error:', error, errorInfo);
}
handleRetry = () => {
this.setState(prevState => ({
hasError: false,
error: null,
retryCount: prevState.retryCount + 1
}));
};
handleReload = () => {
window.location.reload();
};
handleGoHome = () => {
window.location.href = '/';
};
render() {
if (this.state.hasError) {
const { error, retryCount } = this.state;
const isNetworkError = error.message.includes('network') ||
error.message.includes('fetch');
return (
<div className="error-recovery">
<h2>Something went wrong</h2>
{isNetworkError && (
<>
<p>Network connection issue detected.</p>
<button onClick={this.handleRetry}>
Retry ({3 - retryCount} attempts remaining)
</button>
</>
)}
{!isNetworkError && (
<>
<p>An unexpected error occurred.</p>
<button onClick={this.handleReload}>Reload Page</button>
<button onClick={this.handleGoHome}>Go to Home</button>
</>
)}
{retryCount >= 3 && (
<p>
If the problem persists, please contact support.
</p>
)}
</div>
);
}
return this.props.children;
}
}
Best Practice: Place Error Boundaries strategically at different levels—one at the app root for catastrophic errors, and others around features for isolated error handling.
Concurrent Features with Suspense
React 18+ Suspense works with concurrent features for better UX:
import { Suspense, useState, useTransition } from 'react';
function SearchResults() {
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
const handleSearch = (e) => {
const value = e.target.value;
// Mark state update as non-urgent
startTransition(() => {
setQuery(value);
});
};
return (
<div>
<input
type="text"
placeholder="Search..."
onChange={handleSearch}
style={{ opacity: isPending ? 0.5 : 1 }}
/>
<Suspense fallback={<div>Searching...</div>}>
<ResultsList query={query} />
</Suspense>
</div>
);
}
Exercise 1: Error Boundary System
Task: Build a comprehensive error handling system:
- Create an Error Boundary component with custom fallback UI
- Add error logging to console (simulate external service)
- Implement retry functionality
- Create a buggy component that throws errors on button click
- Show different error messages for different error types
Exercise 2: Lazy Loading Dashboard
Task: Build a dashboard with lazy-loaded sections:
- Create separate components for Charts, Stats, and Activity sections
- Lazy load each section with
React.lazy() - Add Suspense boundaries with skeleton loading UI
- Wrap each section in Error Boundaries
- Add a "Reload Section" button in error fallback
Exercise 3: Robust Async Data Loader
Task: Create a reusable async data loader component:
- Combine Error Boundary and Suspense
- Accept render props for loading, error, and success states
- Implement automatic retry with exponential backoff
- Add timeout handling
- Display retry count and estimated next retry time
Summary
In this lesson, you mastered error handling and async management in React:
- Creating Error Boundary components to catch and handle errors gracefully
- Understanding what Error Boundaries can and cannot catch
- Implementing production-ready error logging and tracking
- Using Suspense for lazy loading and async operations
- Combining Error Boundaries and Suspense for robust applications
- Building skeleton loading UIs for better user experience
- Implementing smart error recovery strategies
- Leveraging concurrent features for optimal performance
With these React.js fundamentals mastered, you're ready to build production-quality applications with proper error handling, optimized performance, and excellent user experience!