Handling Loading & Errors in React
Modern web applications rely heavily on asynchronous operations—especially when fetching data from APIs. While this allows your app to stay responsive, it also introduces two important states you must handle properly:
Loading state (when data is being fetched)
Error state (when something goes wrong)
If you ignore these, your UI will feel broken, confusing, or unreliable.
Why Handling Loading and Errors Matters
When your React app fetches data:
The request takes time → user needs feedback (loading state)
The request may fail → user needs clarity (error state)
Without handling these:
Users may see blank screens
Debugging becomes harder
UX becomes poor
The correct approach is to use the useEffect Hook.
Basic Example: Fetching Data with useEffect
Here’s a simple and correct pattern:
import { useState, useEffect } from 'react';
function App() {
const [data, setData] = useState(null);
useEffect(() => {
async function fetchData() {
const response = await fetch('https://example.com/data');
const jsonData = await response.json();
setData(jsonData);
}
fetchData();
}, []);
if (!data) {
return <p>Loading...</p>;
}
return <div>Data: {JSON.stringify(data)}</div>;
}What’s happening here?
useEffectruns once when the component mounts ([])Data is fetched asynchronously
Once received, state updates → component re-renders
Adding Proper Loading and Error Handling
The above example works, but it’s incomplete. Let’s improve it by handling all states.
Improved Version
import { useState, useEffect } from 'react';
function App() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function fetchData() {
try {
setLoading(true);
const response = await fetch('https://example.com/data');
if (!response.ok) {
throw new Error('Failed to fetch data');
}
const jsonData = await response.json();
setData(jsonData);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
fetchData();
}, []);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return <div>Data: {JSON.stringify(data)}</div>;
}Understanding the States Clearly
1. Loading State
const [loading, setLoading] = useState(true);Starts as
trueTurns
falseafter API completes
2. Error State
const [error, setError] = useState(null);Stores error message
Helps display meaningful UI
3. Data State
const [data, setData] = useState(null);Holds API response
Drives UI rendering
Avoiding Common Pitfalls
1. Infinite Loops
useEffect(() => {
fetchData();
});This runs on every render → infinite loop
Fix:
useEffect(() => {
fetchData();
}, []);2. Updating State After Unmount (Memory Leak)
If a component unmounts before the API finishes, calling setState causes warnings.
useEffect(() => {
const controller = new AbortController();
async function fetchData() {
try {
const response = await fetch('https://example.com/data', {
signal: controller.signal,
});
const data = await response.json();
setData(data);
} catch (error) {
if (error.name !== 'AbortError') {
setError(error.message);
}
}
}
fetchData();
return () => {
controller.abort();
};
}, []);3. Multiple Requests Overwriting Each Other
If dependencies change quickly, older responses may override newer ones.
Fix:
Cancel previous requests
Or track request IDs
Better UI Patterns for Loading
Instead of plain text:
<p>Loading...</p>Better
Skeleton loaders
Spinners
Placeholder UI
Example:
return loading ? <Spinner /> : <DataView data={data} />;Real-World Best Practice: Custom Hook
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let isMounted = true;
async function fetchData() {
try {
const res = await fetch(url);
const json = await res.json();
if (isMounted) {
setData(json);
}
} catch (err) {
if (isMounted) {
setError(err.message);
}
} finally {
if (isMounted) {
setLoading(false);
}
}
}
fetchData();
return () => {
isMounted = false;
};
}, [url]);
return { data, loading, error };
}Usage:
function App() {
const { data, loading, error } = useFetch('/api/data');
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return <div>{JSON.stringify(data)}</div>;
}