Declarative Async Handling: Gracefully Managing Loading/Errors with Suspense and Error Boundary

Chris's component code is always scattered. He intends to display a 'User Profile', but when you open the file, more than half of the code is occupied by loading and error checks.
// ❌ Imperative Approach: Focusing on "How"
function UserProfile() {
const { data, isLoading, isError } = useQuery({ ... });
// 1. Loading Check
if (isLoading) return <Spinner />;
// 2. Error Check
if (isError) return <ErrorMessage />;
// 3. Null Data Check
if (!data) return null;
// 4. Finally, the main point (Happy Path)
return <div>{data.name}</div>;
}This code is called "Imperative." It commands what to draw step-by-step based on the state.
Wouldn't it be nice if we could strip away all this annoying check logic and write code assuming "Data definitely exists"?
Using React's Suspense and Error Boundary makes this dreamlike "Declarative" handling possible.
1. Suspense: "Wait Until Ready"
Suspense is a feature fully supported since React 18. When a component signals that it is "not ready to render yet (Suspend)," React finds the nearest parent Suspense and displays a Fallback (Loading Screen) instead.
Combining TanStack Query with Suspense
From v5 onwards, TanStack Query provides a dedicated hook called useSuspenseQuery. This hook 'suspends' the component execution if there is no data. It doesn't even return isLoading. Why? Because if the code passed this line, the data is guaranteed to exist.
// ✅ Declarative Approach: Focusing on "What"
import { useSuspenseQuery } from '@tanstack/react-query';
function UserProfile() {
// No isLoading, No isError. 'data' is guaranteed not to be undefined.
const { data } = useSuspenseQuery({
queryKey: ['user'],
queryFn: fetchUser
});
return <div>{data.name}</div>;
}Now, the parent component takes responsibility for handling the loading state.
// ParentComponent.tsx
import { Suspense } from 'react';
function App() {
return (
// Show this spinner until the child is ready.
<Suspense fallback={<Spinner />}>
<UserProfile />
</Suspense>
);
}By doing this, you don't have to implement loading states in every single component. You can group them by section and manage them collectively.
2. Error Boundary: "Catch Errors from Above"
Just like loading, error handling works similarly. Just as JavaScript's try-catch propagates errors up the stack, errors occurring in React components bubble up like foam.
An Error Boundary captures these errors and displays an error screen instead. rather than implementing it yourself, it is recommended to use the battle-tested library react-error-boundary.
// App.tsx
import { ErrorBoundary } from 'react-error-boundary';
function App() {
return (
<ErrorBoundary fallback={<div>An error occurred!</div>}>
<Suspense fallback={<Spinner />}>
<UserProfile />
</Suspense>
</ErrorBoundary>
);
}Trying Again (Reset)
If the error screen doesn't have a "Try Again" button, users will be frustrated. Using QueryErrorResetBoundary, you can reset the query and resend the request when an error occurs.
// App.tsx
import { QueryErrorResetBoundary } from '@tanstack/react-query';
import { ErrorBoundary } from 'react-error-boundary';
function App() {
return (
<QueryErrorResetBoundary>
{({ reset }) => (
<ErrorBoundary
onReset={reset} // Connect query reset function
fallbackRender={({ resetErrorBoundary }) => (
<div>
Error occurred!
{/* Clicking the button resets the query and triggers Suspense again */}
<button onClick={resetErrorBoundary}>Try Again</button>
</div>
)}
>
<Suspense fallback={<Spinner />}>
<UserProfile />
</Suspense>
</ErrorBoundary>
)}
</QueryErrorResetBoundary>
);
}3. Pros and Cons of Declarative Handling
Chris wants to change all his code to use Suspense. However, there are points to consider.
Pros
Cons and Cautions
Key Takeaways
We have mastered the most modern way to handle loading and errors. Now it's time to maximize User Experience (UX).
When a user clicks the "Like" button, what if we fill the heart immediately without waiting for the server response?
Continuing in: “useMutation and Optimistic Updates: Don't Let the User Feel Even 0.1s of Delay”