Suspense fallbacks, Error Boundary fallbacks, loading skeletons, and empty states are all the same underlying idea: content isn't ready, here's what to show instead. This lesson covers building a consistent, reusable fallback component library with real retry actions and appropriate size variants.
1One Concept, Many Names
A Suspense fallback, an Error Boundary's fallback, a loading skeleton, and an empty state are all the same underlying idea: the real content isn't ready, so here's what to show instead. Recognizing them as one unified concept — fallback UI — encourages designing them consistently rather than ad hoc.
2Designing a Consistent Fallback System
Rather than every part of a codebase inventing its own spinner style or error message tone, a small library of reusable fallback components — LoadingFallback, ErrorFallback, EmptyFallback — with a consistent visual language, used everywhere the same situation occurs, produces a far more cohesive product.
3Retry Actions Belong in Fallback UI
A good error fallback offers a way to try again without a full page reload, not just a message that something went wrong. Wiring a reset mechanism like react-error-boundary's resetErrorBoundary, or a query library's refetch, directly into a fallback's 'Try Again' button gives users a real recovery path.
4Contextual Fallbacks, Not Generic Ones
A shared fallback library shouldn't mean every fallback looks identical regardless of context — a full-page error and a small inline widget error warrant different visual weight. Building shared fallback components with size or variant props keeps consistency without forcing a one-size-fits-all appearance.
5Step-by-Step Breakdown
One Concept, Many Names. A Suspense fallback, an Error Boundary's fallback, a loading skeleton, an empty state — they're all the SAME underlying idea: 'the real content isn't ready, here's what to show instead.' Thinking of them as one unified concept, 'fallback UI,' helps you design them consistently instead of ad hoc.
Designing a Consistent Fallback System. Instead of every team member inventing their own spinner style or error message tone, build a small library of reusable fallback components — <LoadingFallback />, <ErrorFallback />, <EmptyFallback /> — with a consistent visual language, used everywhere the same situation occurs.
Why build a shared library of <LoadingFallback />, <ErrorFallback />, and <EmptyFallback /> components instead of writing custom UI for each case ad hoc?
- →It ensures a consistent visual language across the entire app
- →It's the only way to reduce the JavaScript bundle size
Retry Actions Belong in Fallback UI. A good error fallback doesn't just say 'something went wrong' — it gives the user a way to try again, without a full page reload. react-error-boundary's resetErrorBoundary (or TanStack Query's refetch) can be wired directly into a 'Try Again' button in your fallback component.
Contextual Fallbacks, Not Generic Ones. A shared fallback library shouldn't mean every fallback looks identical everywhere — a full-page error and a small inline widget error need different visual weight. Build your shared fallback components with size/variant props (size="small" vs. size="page") so consistency doesn't mean one-size-fits-all.
Mastery Achieved. You now understand fallback UI as a unified concept: loading skeletons, error messages, and empty states are all answers to the same question, best built as a shared, consistent component library with real retry actions and appropriate size variants. Next, you'll learn Suspense Patterns for orchestrating these fallbacks declaratively.
Level Up 🚀
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported.
Fully supported.
Fully supported.
Fully supported.
Accessibility (A11y)
1A Shared Fallback Library Is a Great Place to Standardize Accessible Markup
Building role='status'/'alert', proper aria-live usage, and clear labeling once into shared LoadingFallback and ErrorFallback components guarantees every usage across the app inherits correct accessibility behavior, instead of relying on every individual usage to remember it.
SEO Implications
- 1
Fallback UI Governs Client-Side Interaction States
This system affects what's shown during client-side loading, error, and empty conditions after hydration, with no direct bearing on the initial server-rendered HTML seen by crawlers.
Best Practices
Centralize Fallback Components in a Shared Location
Keep LoadingFallback, ErrorFallback, and EmptyFallback in a shared/ or common/ folder so every feature imports the same consistent implementations rather than reinventing similar UI repeatedly.
Always Include a Recovery Path in Error Fallbacks Where Possible
A retry button, a link back to a working page, or a support contact gives users somewhere to go — an error fallback with no next step feels like a dead end.
Frequent Bugs
Different parts of the app show noticeably inconsistent loading spinners and error message styles.
Consolidate ad hoc loading and error UI into shared LoadingFallback and ErrorFallback components, replacing one-off implementations scattered across the codebase.
An error fallback shows a message but gives the user no way to recover without a full page reload.
Wire a retry mechanism (resetErrorBoundary from react-error-boundary, or a query library's refetch) into a 'Try Again' button within the fallback component.
Real-World Examples
A Shared Fallback Library Used Across Features
A team noticed every feature had built its own slightly different loading spinner and error message. Centralizing LoadingFallback, ErrorFallback (with a built-in retry button), and EmptyFallback (accepting title/message/action props) into a shared/fallbacks/ folder, then swapping every ad hoc implementation to use them, gave the whole app a consistent, polished feel with far less duplicated code.
// shared/fallbacks/ErrorFallback.tsx
export function ErrorFallback({ error, resetErrorBoundary, size = 'inline' }) {
return (
<div role="alert" className={`error-fallback error-fallback--${size}`}>
<p>{error.message}</p>
<button onClick={resetErrorBoundary}>Try Again</button>
</div>
);
}