Suspense catches loading states but not errors—without an Error Boundary, thrown promises and component errors will crash your app. Wrap Suspense with ErrorBoundary to gracefully handle both loading and error states.
Suspense is a React feature that lets you pause rendering while data loads, displaying a fallback UI in the meantime. However, Suspense only handles loading states—it cannot catch or handle errors that occur during async operations. When a promise rejects or a component within Suspense throws an error, React needs an Error Boundary to catch it. Without an Error Boundary wrapping your Suspense, the error propagates up and crashes your entire application or component tree. This error occurs because you're relying on Suspense alone to manage both loading and error states, but Suspense is not designed to catch errors. You need to pair Suspense with React's Error Boundary component to create a robust error handling pattern.
The primary solution is to nest your Suspense component inside an Error Boundary. You can use React's built-in Error Boundary API or the popular react-error-boundary library.
Using react-error-boundary:
npm install react-error-boundaryimport { ErrorBoundary } from "react-error-boundary";
import { Suspense } from "react";
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div role="alert">
<p>Something went wrong loading this data:</p>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
export function MyComponent() {
return (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Suspense fallback={<div>Loading...</div>}>
<DataComponent />
</Suspense>
</ErrorBoundary>
);
}If you prefer not to use an external library, create a class-based Error Boundary component:
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
console.error("Error caught by boundary:", error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div>
<h2>Something went wrong</h2>
<p>{this.state.error?.message}</p>
<button onClick={() => this.setState({ hasError: false })}>
Try again
</button>
</div>
);
}
return this.props.children;
}
}
export function MyComponent() {
return (
<ErrorBoundary>
<Suspense fallback={<div>Loading...</div>}>
<DataComponent />
</Suspense>
</ErrorBoundary>
);
}If using SWR or TanStack Query, ensure error boundaries are enabled. These libraries can throw errors into Suspense which then need to be caught.
SWR Example:
import useSWR from "swr";
import { Suspense } from "react";
import { ErrorBoundary } from "react-error-boundary";
function DataComponent() {
const { data, error } = useSWR("/api/data", fetcher, {
suspense: true, // Enable Suspense
useErrorBoundary: true, // Enable error boundary integration
});
return <div>{data.name}</div>;
}
export function MyComponent() {
return (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Suspense fallback={<div>Loading...</div>}>
<DataComponent />
</Suspense>
</ErrorBoundary>
);
}TanStack Query Example:
import { useQuery } from "@tanstack/react-query";
import { Suspense } from "react";
import { ErrorBoundary } from "react-error-boundary";
function DataComponent() {
const { data, error } = useQuery({
queryKey: ["data"],
queryFn: fetchData,
suspense: true, // Enable Suspense
throwOnError: true, // Throw errors into error boundary
});
return <div>{data.name}</div>;
}
export function MyComponent() {
return (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Suspense fallback={<div>Loading...</div>}>
<DataComponent />
</Suspense>
</ErrorBoundary>
);
}Error Boundaries must be positioned in your component tree so they wrap the Suspense boundary. The order matters:
✓ Correct - Error Boundary wraps Suspense:
<ErrorBoundary>
<Suspense fallback={<Loading />}>
<DataComponent />
</Suspense>
</ErrorBoundary>✗ Wrong - Suspense wraps Error Boundary:
<Suspense fallback={<Loading />}>
<ErrorBoundary>
<DataComponent />
</ErrorBoundary>
</Suspense>The Error Boundary must be the outer wrapper so it can catch errors thrown from within Suspense.
Verify your Error Boundary is properly configured by testing with an intentional error:
function TestComponent() {
const [shouldError, setShouldError] = React.useState(false);
if (shouldError) {
throw new Error("Test error in Suspense");
}
return (
<div>
<p>Data loaded successfully</p>
<button onClick={() => setShouldError(true)}>
Trigger Error
</button>
</div>
);
}
export function MyComponent() {
return (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Suspense fallback={<div>Loading...</div>}>
<TestComponent />
</Suspense>
</ErrorBoundary>
);
}Click the button to verify your error boundary catches and displays the error properly.
Error Boundary Limitations:
Error Boundaries only catch errors in the render method and lifecycle methods. Errors thrown in event handlers, async callbacks, or setTimeout must be caught separately. For those cases, use try-catch blocks or reject the promise so Suspense can catch it.
Development vs Production:
In development mode, React displays an error overlay even when an Error Boundary catches the error. This is intentional—you can dismiss it with the X button. In production, the Error Boundary's fallback UI will be shown instead.
Suspense in Next.js:
When using Suspense in Next.js server components, error handling works differently. Server component errors require a separate error.tsx file at the segment level, while client component Suspense errors need Error Boundaries as described above.
Avoiding Common Pitfalls:
- Don't put the Error Boundary inside the Suspense
- Don't use try-catch around Suspense in the JSX
- Ensure your data fetching library is configured to throw errors (useErrorBoundary option)
- Remember that Suspense only works with code that throws promises, not with traditional callbacks
Prop spreading could cause security issues
Prop spreading could cause security issues
Error: error:0308010C:digital envelope routines::unsupported
Error: error:0308010C:digital envelope routines::unsupported
React Hook "useEffect" is called conditionally. React Hooks must be called in the exact same order in every component render.
React Hook useEffect placed inside a condition
Hook can only be called inside the body of a function component
Hook can only be called inside the body of a function component
Rollup failed to resolve import during build
How to fix "Rollup failed to resolve import" in React