Handling Loading and Error States
Add loading and error state variables to your component, show a spinner while fetching, and display a friendly error message on failure.
Handling Loading and Error States is a free Frontend Academy lesson on CoddyKit — lesson 3 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Frontend Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
The Three States of Async Data
Any async data operation has three states: loading (request in flight), success (data received), error (request failed). Your UI should handle all three explicitly.
Loading UI
While loading, show a spinner, skeleton, or placeholder. Prevent form resubmission by disabling buttons during loading. The user should always know the app is working.
if (loading) {
return (
<div className="loading-state">
<Spinner size="lg" />
<p>Loading users...</p>
</div>
);
}Error UI
Show a friendly error message when a request fails. Include a retry action so users aren't stuck. Log the full error for developers.
if (error) {
return (
<div className="error-state">
<p>Failed to load data: {error}</p>
<button onClick={() => setRetry(n => n + 1)}>Try Again</button>
</div>
);
}Retry Mechanism
Increment a retry counter in state and add it to the effect's dependency array. When the user clicks Retry, the counter changes, triggering the effect again.
const [retry, setRetry] = useState(0);
useEffect(() => {
// fetch...
}, [userId, retry]); // retry in deps triggers re-fetch
<button onClick={() => setRetry(n => n + 1)}>Retry</button>Loading State Best Practices
1) Show loading state immediately on request. 2) Keep loading state minimal — don't re-set it if cached data is available. 3) For pagination, distinguish initial load from loading-more. 4) Never flash a spinner for under 100ms — add a delay or keep previous data visible.
Optimistic Updates
Optimistic updates: update the UI immediately before the server confirms, then roll back if the request fails. Gives the impression of instant responsiveness.
// Optimistic:
const prevItems = items;
setItems(items.filter(i => i.id !== id)); // immediate UI update
try {
await deleteItem(id);
} catch (err) {
setItems(prevItems); // revert on failure
showError('Delete failed. Changes reverted.');
}Error Boundaries for Render Errors
Use Error Boundaries (class components with componentDidCatch) to catch errors thrown during rendering. They show fallback UI instead of crashing the entire app.
HTTP Error Mapping
Map HTTP status codes to user-friendly messages. 401 → redirect to login. 403 → show 'Access denied'. 404 → 'Not found'. 429 → 'Too many requests — please wait'. 500+ → 'Server error — try again'.
function mapStatusToMessage(status: number): string {
if (status === 401) return 'Please sign in to continue';
if (status === 403) return 'You don\'t have permission to do this';
if (status === 404) return 'This item no longer exists';
if (status === 429) return 'Too many requests. Please wait a moment';
if (status >= 500) return 'Server error. We\'re looking into it';
return 'Something went wrong';
}useReducer for Complex Fetch State
When loading, error, and data interact (e.g., clearing error when re-loading), a reducer is cleaner than multiple useState calls.
type FetchState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; message: string };
const [state, dispatch] = useReducer(fetchReducer, { status: 'idle' });Suspense — The Future
React Suspense with data fetching frameworks (React Query, Relay, Next.js) lets components 'suspend' while awaiting data. A parent Suspense boundary shows fallback UI automatically. This is the direction React is heading.
Network Error vs Server Error
Network errors (no connection, CORS) throw in the catch block. Server errors (4xx, 5xx) resolve normally — you must check res.ok to detect them. Your error handling must address both categories.
Quick Check
What is an 'optimistic update'?
Recap: Loading and Error States
Always handle loading, success, and error states. Show meaningful loading UI. Map HTTP errors to user-friendly messages. Implement retry mechanisms. Optimistic updates for instant perceived feedback. useReducer for coordinated fetch state. Suspense will eventually simplify this pattern further.
Frequently asked questions
Is the “Handling Loading and Error States” lesson free?
Yes — the full text of “Handling Loading and Error States” is free to read here on the web, and the Frontend Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Frontend Academy course, upgrade to CoddyKit PRO.
What will I learn in “Handling Loading and Error States”?
Add loading and error state variables to your component, show a spinner while fetching, and display a friendly error message on failure. You practise Frontend Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start Frontend Academy?
No prior experience is required. Frontend Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 3 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Handling Loading and Error States” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this Frontend Academy lesson?
Yes. Every Frontend Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- useEffect Basics: Dependencies and Cleanup
- Fetching Data on Mount
- Handling Loading and Error States
- AbortController for Cleanup