React Suspense, complete.
ErrorBoundary, Suspense, Delay, and more — so you can focus on the success case.
<ErrorBoundary />
Declarative error handling with fallback, resetKeys, onError, and shouldCatch for selective error catching.
<ErrorBoundary
shouldCatch={NetworkError}
fallback={({ error, reset }) => <ErrorUI error={error} onRetry={reset} />}
>
<App />
</ErrorBoundary>Learn more<Suspense clientOnly />
SSR-safe Suspense boundary. Avoids hydration mismatches in Next.js without dynamic() or useEffect guards.
Learn more<Delay ms={200} />
Prevent flash-of-loading-state. Show spinners only when loading actually takes time. Supports render props for fade-in.
Learn more<DefaultPropsProvider />
Set global default fallbacks for Suspense and Delay. Override per-component when needed.
Learn moreAlready using react-error-boundary?
You’re on the right track.
react-error-boundary is a great library that made declarative error handling practical in React. Suspensive’s ErrorBoundary adds a few things we found useful in production — shouldCatch for catching only specific errors, ErrorBoundaryGroup for resetting multiple boundaries at once, and safe fallback error propagation. We also needed to solve loading states, SSR hydration, flash-of-loading UX, and data fetching — so we built those too, all in one package.
| Suspensive | react-error-boundary | React Class | ||
|---|---|---|---|---|
| Error Boundary | Selective error catching (shouldCatch) | ✓ | — | — |
| Group reset (ErrorBoundaryGroup) | ✓ | — | — | |
| useErrorBoundaryFallbackProps | ✓ | — | — | |
| Safe fallback error propagation | ✓ | — | — | |
| Declarative ErrorBoundary | ✓ | ✓ | — | |
| resetKeys | ✓ | ✓ | — | |
| Basic error catching | ✓ | ✓ | ✓ | |
| Async Rendering | SSR-safe Suspense (clientOnly) | ✓ | — | — |
| Flash-of-loading prevention (Delay) | ✓ | — | — | |
| Global default fallbacks (DefaultPropsProvider) | ✓ | — | — | |
| Declarative data fetching (SuspenseQuery) | ✓ | — | — | |
| Client-only rendering (ClientOnly) | ✓ | — | — |
See it in action
How loading, errors, and recovery actually work.
Without Suspense, a typical TanStack Query page looks like this.
import { useQuery } from '@tanstack/react-query'const Page = () => {const userQuery = useQuery(userQueryOptions())const postsQuery = useQuery({...postsQueryOptions(),select: (posts) => posts.filter(({ isPublic }) => isPublic),})const promotionsQuery = useQuery(promotionsQueryOptions())if (userQuery.isLoading ||postsQuery.isLoading ||promotionsQuery.isLoading) {return 'loading...'}if (userQuery.isError || postsQuery.isError || promotionsQuery.isError) {return 'error'}return (<Fragment><UserProfile {...userQuery.data} />{postsQuery.data.map((post) => (<PostListItem key={post.id} {...post} />))}{promotionsQuery.data.map((promotion) => (<Promotion key={promotion.id} {...promotion} />))}</Fragment>)}
Suspense makes it type-safe. But hooks force you to split components.
import { ErrorBoundary, Suspense } from '@suspensive/react'import { useSuspenseQuery } from '@tanstack/react-query'const Page = () => (<ErrorBoundary fallback="error"><Suspense fallback="loading..."><UserInfo userId={userId} /><PostList userId={userId} /><PromotionList userId={userId} /></Suspense></ErrorBoundary>)const UserInfo = ({ userId }) => {const { data: user } = useSuspenseQuery(userQueryOptions())return <UserProfile {...user} />}const PostList = ({ userId }) => {const { data: posts } = useSuspenseQuery({...postsQueryOptions(),select: (posts) => posts.filter(({ isPublic }) => isPublic),})return posts.map((post) => <PostListItem key={post.id} {...post} />)}const PromotionList = ({ userId }) => {const { data: promotions } = useSuspenseQuery(promotionsQueryOptions())return promotions.map((promotion) => (<PromotionListItem key={promotion.id} {...promotion} />))}
With Suspensive, everything stays at the same depth.
import { ErrorBoundary, Suspense } from '@suspensive/react'import { SuspenseQuery } from '@suspensive/react-query'const Page = () => (<ErrorBoundary fallback="error"><Suspense fallback="loading..."><SuspenseQuery {...userQueryOptions()}>{({ data: user }) => <UserProfile {...user} />}</SuspenseQuery><SuspenseQuery{...postsQueryOptions()}select={(posts) => posts.filter(({ isPublic }) => isPublic)}>{({ data: posts }) =>posts.map((post) => <PostListItem key={post.id} {...post} />)}</SuspenseQuery><SuspenseQuery{...promotionsQueryOptions()}select={(promotions) => promotions.filter(({ isPublic }) => isPublic)}>{({ data: promotions }) =>promotions.map((promotion) => (<PromotionListItem key={promotion.id} {...promotion} />))}</SuspenseQuery></Suspense></ErrorBoundary>)
Playground
Edit, break, and recover — all in the browser.