Error Handling
Things break. APIs go down, networks hiccup, users do unexpected things. Good error handling is like wearing a seatbelt — you hope you never need it, but you are glad it is there when you do.
React gives you several tools to catch and handle errors gracefully. Let us walk through them.
Try Catch Blocks
The most basic approach wraps your risky code in try/catch. If something goes wrong, you catch it instead of letting your whole app crash.
import { useState, useEffect } from 'react'
function DataFetcher() {
const [data, setData] = useState(null)
const [error, setError] = useState(null)
useEffect(() => {
const fetchData = async () => {
try {
const res = await fetch('/api/data')
if (!res.ok) throw new Error('Network response was not ok')
const json = await res.json()
setData(json)
} catch (err) {
setError(err.message)
}
}
fetchData()
}, [])
if (error) return Oops: {error}
if (!data) return Loading...
return {data.title}
}
The error state catches problems and displays a friendly message. Users see something helpful instead of a blank screen or crash.
Error Boundaries
Error boundaries are like safety nets for your component tree. When a child component throws an error, the boundary catches it and shows a fallback instead of crashing everything.
import { Component } from 'react'
class ErrorBoundary extends Component {
constructor(props) {
super(props)
this.state = { hasError: false, error: null }
}
static getDerivedStateFromError(error) {
return { hasError: true, error }
}
render() {
if (this.state.hasError) {
return (
Something went wrong
{this.state.error?.message}
)
}
return this.props.children
}
}
Wrap your components in an error boundary and problems stay contained. One broken component does not take down your entire page.
Retry Mechanisms
Sometimes errors are temporary. A network glitch passes, a server recovers. Giving users a retry button turns a failure into a minor delay.
import { useState, useEffect, useCallback } from 'react'
function RetryFetcher() {
const [data, setData] = useState(null)
const [error, setError] = useState(null)
const [loading, setLoading] = useState(false)
const fetchData = useCallback(async () => {
setLoading(true)
setError(null)
try {
const res = await fetch('/api/data')
if (!res.ok) throw new Error('Failed to load')
const json = await res.json()
setData(json)
} catch (err) {
setError(err.message)
} finally {
setLoading(false)
}
}, [])
useEffect(() => { fetchData() }, [fetchData])
if (loading) return Loading...
if (error) {
return (
Error: {error}
)
}
return {data?.content}
}
The retry button calls the same fetch function again. Simple, effective, and keeps users in control.
Try it Yourself →