Errors happen in every application - network failures, unexpected data, bugs, and user mistakes. Without proper error handling, a single error can crash your entire React app, leaving users with a blank screen. In this lesson, you'll learn how to catch and handle errors gracefully using error boundaries, implement recovery strategies, and provide great user experiences even when things go wrong. Let's build resilient React applications! ๐ก๏ธ
Why Error Handling Matters
Without Error Handling
function UserProfile({ userId }) {
const [user, setUser] = React.useState(null)
React.useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => setUser(data))
// โ No error handling!
}, [userId])
// โ What if user is null or undefined?
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
)
}
// Problems:
// - App crashes if fetch fails
// - App crashes if user is null
// - User sees blank screen
// - No feedback about what went wrongWith Error Handling
function UserProfile({ userId }) {
const [user, setUser] = React.useState(null)
const [loading, setLoading] = React.useState(true)
const [error, setError] = React.useState(null)
React.useEffect(() => {
setLoading(true)
setError(null)
fetch(`/api/users/${userId}`)
.then(res => {
if (!res.ok) throw new Error('Failed to fetch user')
return res.json()
})
.then(data => {
setUser(data)
setLoading(false)
})
.catch(err => {
setError(err.message)
setLoading(false)
})
}, [userId])
if (loading) return <div>Loading...</div>
if (error) return <div>Error: {error}</div>
if (!user) return <div>User not found</div>
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
)
}
// Benefits:
// โ
App doesn't crash
// โ
User sees helpful messages
// โ
Clear loading state
// โ
Graceful degradationError Boundaries
Error boundaries are React components that catch JavaScript errors anywhere in their child component tree:
Creating an Error Boundary
import React from 'react'
class ErrorBoundary extends React.Component {
constructor(props) {
super(props)
this.state = { hasError: false, error: null, errorInfo: null }
}
// Called when error is thrown
static getDerivedStateFromError(error) {
// Update state to show fallback UI
return { hasError: true }
}
// Called after error is caught
componentDidCatch(error, errorInfo) {
// Log error to error reporting service
console.error('Error caught by boundary:', error, errorInfo)
// Store error details
this.setState({
error: error,
errorInfo: errorInfo
})
// Send to error tracking service
// logErrorToService(error, errorInfo)
}
render() {
if (this.state.hasError) {
// Fallback UI
return (
<div className="error-container">
<h1>Something went wrong</h1>
<details style={{ whiteSpace: 'pre-wrap' }}>
{this.state.error && this.state.error.toString()}
<br />
{this.state.errorInfo.componentStack}
</details>
</div>
)
}
// No error, render children normally
return this.props.children
}
}
export default ErrorBoundary
// Usage:
function App() {
return (
<ErrorBoundary>
<UserDashboard />
</ErrorBoundary>
)
}
// If UserDashboard or any child throws error:
// 1. Error boundary catches it
// 2. Shows fallback UI
// 3. Rest of app keeps working!Better Error Boundary with Reset
class ErrorBoundary extends React.Component {
constructor(props) {
super(props)
this.state = { hasError: false, error: null }
}
static getDerivedStateFromError(error) {
return { hasError: true, error }
}
componentDidCatch(error, errorInfo) {
console.error('Error:', error, errorInfo)
}
// Reset error state
resetError = () => {
this.setState({ hasError: false, error: null })
}
render() {
if (this.state.hasError) {
return (
<div className="error-boundary">
<h2>Oops! Something went wrong</h2>
<p>{this.state.error?.message}</p>
<button onClick={this.resetError}>
Try Again
</button>
</div>
)
}
return this.props.children
}
}
// With custom fallback:
function App() {
return (
<ErrorBoundary fallback={<CustomErrorPage />}>
<Dashboard />
</ErrorBoundary>
)
}What Error Boundaries Catch:
- โ Errors in render methods
- โ Errors in lifecycle methods
- โ Errors in constructor
- โ Errors in child components
What Error Boundaries DON'T Catch:
- โ Event handlers (use try-catch)
- โ Async code (setTimeout, promises)
- โ Server-side rendering errors
- โ Errors in the boundary itself
Where to Place Error Boundaries
Strategy 1: Top-Level Boundary
// Catches all errors, shows full-page error
function App() {
return (
<ErrorBoundary fallback={<FullPageError />}>
<Router>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</Router>
</ErrorBoundary>
)
}
// Pros: Simple, catches everything
// Cons: Any error breaks entire appStrategy 2: Route-Level Boundaries
// Each route has its own boundary
function App() {
return (
<Router>
<Routes>
<Route
path="/"
element={
<ErrorBoundary fallback={<RouteError />}>
<Home />
</ErrorBoundary>
}
/>
<Route
path="/dashboard"
element={
<ErrorBoundary fallback={<RouteError />}>
<Dashboard />
</ErrorBoundary>
}
/>
</Routes>
</Router>
)
}
// Pros: Errors isolated to routes
// Cons: More boundaries to manageStrategy 3: Component-Level Boundaries (Best)
// Boundaries around critical features
function Dashboard() {
return (
<div>
<Header />
<ErrorBoundary fallback={<SectionError section="stats" />}>
<UserStats />
</ErrorBoundary>
<ErrorBoundary fallback={<SectionError section="activity" />}>
<RecentActivity />
</ErrorBoundary>
<ErrorBoundary fallback={<SectionError section="notifications" />}>
<Notifications />
</ErrorBoundary>
</div>
)
}
// Pros:
// โ
Fine-grained error isolation
// โ
Rest of page works if one section fails
// โ
Better UX
// Best practice: Multiple boundaries at different levels!Handling Async Errors
Error boundaries don't catch async errors. Use try-catch:
Async/Await with Try-Catch
function UserProfile({ userId }) {
const [user, setUser] = React.useState(null)
const [loading, setLoading] = React.useState(false)
const [error, setError] = React.useState(null)
React.useEffect(() => {
async function fetchUser() {
setLoading(true)
setError(null)
try {
const response = await fetch(`/api/users/${userId}`)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const data = await response.json()
setUser(data)
} catch (err) {
setError(err.message)
console.error('Failed to fetch user:', err)
} finally {
setLoading(false)
}
}
fetchUser()
}, [userId])
if (loading) return <LoadingSpinner />
if (error) return <ErrorMessage error={error} />
if (!user) return <NotFound />
return <UserCard user={user} />
}
// Pattern: Always wrap async calls in try-catch!Event Handler Errors
function SubmitForm() {
const [error, setError] = React.useState(null)
const [success, setSuccess] = React.useState(false)
const handleSubmit = async (e) => {
e.preventDefault()
setError(null)
setSuccess(false)
try {
const formData = new FormData(e.target)
const response = await fetch('/api/submit', {
method: 'POST',
body: formData
})
if (!response.ok) {
throw new Error('Submission failed')
}
setSuccess(true)
} catch (err) {
setError(err.message)
// Optionally: Log to error service
}
}
return (
<form onSubmit={handleSubmit}>
{error && <div className="error">{error}</div>}
{success && <div className="success">Submitted!</div>}
<input name="email" type="email" required />
<button type="submit">Submit</button>
</form>
)
}
// Error boundaries DON'T catch event handler errors
// Always use try-catch in event handlers!Interactive Demo
๐ก Tip: Edit the code above and click "Run" to see your changes
Error Recovery Patterns
1. Retry Pattern
function DataFetcher({ url }) {
const [data, setData] = React.useState(null)
const [error, setError] = React.useState(null)
const [retryCount, setRetryCount] = React.useState(0)
const fetchData = async () => {
try {
const response = await fetch(url)
if (!response.ok) throw new Error('Fetch failed')
const data = await response.json()
setData(data)
setError(null)
} catch (err) {
setError(err.message)
}
}
React.useEffect(() => {
fetchData()
}, [retryCount])
const handleRetry = () => {
setRetryCount(count => count + 1)
}
if (error) {
return (
<div>
<p>Error: {error}</p>
<button onClick={handleRetry}>Retry</button>
</div>
)
}
return data ? <DisplayData data={data} /> : <Loading />
}2. Fallback Content Pattern
function UserAvatar({ userId }) {
const [user, setUser] = React.useState(null)
const [error, setError] = React.useState(false)
React.useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(setUser)
.catch(() => setError(true))
}, [userId])
if (error) {
// Show default avatar when error
return <DefaultAvatar />
}
return user ? <img src={user.avatar} /> : <LoadingAvatar />
}
// Graceful degradation: Show something instead of nothing3. Optimistic UI Pattern
function TodoList() {
const [todos, setTodos] = React.useState([])
const addTodo = async (text) => {
// Optimistically add to UI
const tempId = Date.now()
const newTodo = { id: tempId, text, temp: true }
setTodos([...todos, newTodo])
try {
// Try to save to server
const response = await fetch('/api/todos', {
method: 'POST',
body: JSON.stringify({ text })
})
const savedTodo = await response.json()
// Replace temp todo with real one
setTodos(todos =>
todos.map(t => t.id === tempId ? savedTodo : t)
)
} catch (err) {
// Revert on error
setTodos(todos => todos.filter(t => t.id !== tempId))
alert('Failed to save todo')
}
}
return <div>{/* UI */}</div>
}
// Show update immediately, revert if failsUser-Friendly Error Messages
// โ Bad - Technical errors shown to users
"TypeError: Cannot read property 'name' of undefined"
"Network request failed with status 500"
"Uncaught ReferenceError: user is not defined"
// โ
Good - Friendly, actionable messages
function getErrorMessage(error) {
// Map technical errors to user-friendly messages
if (error.message.includes('Network')) {
return 'Unable to connect. Please check your internet connection.'
}
if (error.message.includes('404')) {
return 'The requested item could not be found.'
}
if (error.message.includes('403')) {
return 'You do not have permission to access this resource.'
}
if (error.message.includes('500')) {
return 'Server error. Please try again later.'
}
// Generic fallback
return 'Something went wrong. Please try again.'
}
function ErrorMessage({ error }) {
const message = getErrorMessage(error)
return (
<div className="error-banner">
<h3>Oops!</h3>
<p>{message}</p>
<button onClick={retry}>Try Again</button>
<a href="/support">Contact Support</a>
</div>
)
}
// Tips for error messages:
// โ
Clear and specific
// โ
Suggest actions
// โ
Avoid technical jargon
// โ
Provide next steps
// โ
Show empathyError Logging and Monitoring
Logging Errors
// Error logging service
class ErrorLogger {
static log(error, errorInfo, context = {}) {
// Log to console in development
if (process.env.NODE_ENV === 'development') {
console.error('Error:', error)
console.error('Error Info:', errorInfo)
console.error('Context:', context)
}
// Send to error tracking service in production
if (process.env.NODE_ENV === 'production') {
// Example: Sentry, LogRocket, Bugsnag
// Sentry.captureException(error, {
// contexts: { react: errorInfo },
// tags: context
// })
}
// Store in local storage for debugging
try {
const errors = JSON.parse(localStorage.getItem('errors') || '[]')
errors.push({
error: error.toString(),
errorInfo: errorInfo,
context,
timestamp: new Date().toISOString()
})
localStorage.setItem('errors', JSON.stringify(errors.slice(-10)))
} catch (e) {
// Ignore storage errors
}
}
}
// Usage in Error Boundary
class ErrorBoundary extends React.Component {
componentDidCatch(error, errorInfo) {
ErrorLogger.log(error, errorInfo, {
userId: this.props.userId,
route: window.location.pathname
})
}
render() {
// ...
}
}Integrating Error Tracking Services
// Sentry example
import * as Sentry from "@sentry/react"
// Initialize
Sentry.init({
dsn: "your-sentry-dsn",
environment: process.env.NODE_ENV,
integrations: [new Sentry.BrowserTracing()],
tracesSampleRate: 1.0,
})
// Error Boundary with Sentry
const SentryErrorBoundary = Sentry.ErrorBoundary
function App() {
return (
<SentryErrorBoundary fallback={<ErrorFallback />}>
<YourApp />
</SentryErrorBoundary>
)
}
// Manual error reporting
try {
riskyOperation()
} catch (error) {
Sentry.captureException(error)
}
// Add context to errors
Sentry.setUser({ id: user.id, email: user.email })
Sentry.setTag("section", "checkout")
// Popular error tracking services:
// - Sentry (open source, popular)
// - LogRocket (session replay)
// - Bugsnag
// - Rollbar
// - TrackJSError Handling Best Practices
- Use multiple error boundaries - Isolate errors to sections
- Always catch async errors - Use try-catch in async functions
- Show user-friendly messages - Hide technical details
- Provide recovery options - Retry, go back, refresh
- Log errors for debugging - Use error tracking services
- Test error scenarios - Simulate failures
- Handle network errors gracefully - Offline support
- Validate data before use - Check for null/undefined
- Set up error monitoring - Track errors in production
- Document error handling strategy - For team consistency
Error Handling Checklist:
- โ Error boundaries at multiple levels
- โ Try-catch in all async functions
- โ Try-catch in all event handlers
- โ Graceful fallback UI
- โ User-friendly error messages
- โ Retry/recovery mechanisms
- โ Error logging/monitoring setup
- โ Null/undefined checks
- โ Network error handling
- โ Loading and error states
Key Takeaways
- Error boundaries catch render errors in child components
- Use
getDerivedStateFromErrorfor fallback UI - Use
componentDidCatchfor logging errors - Place boundaries at multiple levels for isolation
- Error boundaries DON'T catch async or event handler errors
- Always use try-catch for async/await code
- Always use try-catch in event handlers
- Provide retry and recovery options
- Show user-friendly error messages
- Log errors to monitoring services
- Validate data before using it
- Handle network failures gracefully
- Test error scenarios during development
What's Next?
You now know how to handle errors gracefully in React applications! You understand error boundaries, async error handling, and recovery strategies.
In the final lesson, you'll learn about Testing React Components:
- Introduction to testing with Jest
- Using React Testing Library
- Writing effective component tests
- Testing user interactions
- Testing best practices
Get ready to build reliable, well-tested React applications! ๐งช