UNPKG

@suspensive/react

Version:

Suspensive interfaces for react

397 lines (300 loc) 11.5 kB
--- name: error-handling description: > Layered error handling with @suspensive/react: ErrorBoundary (fallback, shouldCatch, resetKeys, onReset, onError), ErrorBoundary.Consumer, useErrorBoundary().setError, useErrorBoundaryFallbackProps, ErrorBoundaryGroup, ErrorBoundaryGroup.Consumer, blockOutside, useErrorBoundaryGroup, ErrorBoundaryFallbackProps type. Load when catching, filtering, resetting, or propagating render errors. metadata: type: sub-skill library: '@suspensive/react' library_version: '3.21.2' framework: react requires: - 'react' sources: - 'toss/suspensive:docs/suspensive.org/src/content/en/docs/react/ErrorBoundary.mdx' - 'toss/suspensive:docs/suspensive.org/src/content/en/docs/react/ErrorBoundaryGroup.mdx' - 'toss/suspensive:packages/react/src/ErrorBoundary.tsx' --- # Error handling with ErrorBoundary and ErrorBoundaryGroup This skill builds on react. Read ../SKILL.md first for the client-only constraint, the `.with()` HOC pattern, and removed legacy APIs. ## Setup ```tsx 'use client' import { ErrorBoundary } from '@suspensive/react' import { UserProfile } from './UserProfile' export const UserSection = () => ( <ErrorBoundary fallback={({ error, reset }) => ( <div> <p>{error.message}</p> <button onClick={reset}>Try again</button> </div> )} onError={(error, info) => console.error(error, info)} onReset={() => console.log('reset')} > <UserProfile /> </ErrorBoundary> ) ``` `fallback` is the single fallback prop (element or `({ error, reset }) => ReactNode`). Type a standalone fallback component with `ErrorBoundaryFallbackProps`. ## Core Patterns ### Catch only one error type, let the rest propagate ```tsx 'use client' import { ErrorBoundary } from '@suspensive/react' import { NetworkError } from './errors' import { Feed } from './Feed' export const Example = () => ( <ErrorBoundary fallback={({ error }) => <p>Unexpected: {error.message}</p>}> <ErrorBoundary shouldCatch={NetworkError} fallback={({ error, reset }) => <button onClick={reset}>Network failed, retry: {error.message}</button>} > <Feed /> </ErrorBoundary> </ErrorBoundary> ) ``` `shouldCatch` accepts an ErrorConstructor, a callback `(error) => boolean`, a boolean, or an array mixing all three; uncaught errors propagate to the parent boundary. ### Reset every boundary on the page at once ```tsx 'use client' import { ErrorBoundary, ErrorBoundaryGroup } from '@suspensive/react' import { StatsSection, UserSection } from './sections' export const Dashboard = () => ( <ErrorBoundaryGroup> <ErrorBoundaryGroup.Consumer> {(group) => <button onClick={group.reset}>Reset all</button>} </ErrorBoundaryGroup.Consumer> <ErrorBoundary fallback={({ error }) => <p>{error.message}</p>}> <UserSection /> </ErrorBoundary> <ErrorBoundary fallback={({ error }) => <p>{error.message}</p>}> <StatsSection /> </ErrorBoundary> </ErrorBoundaryGroup> ) ``` Nested groups are also reset by the parent group unless they set `blockOutside`; `useErrorBoundaryGroup().reset` does the same from any child component. ### Trigger the boundary without throwing during render ```tsx 'use client' import { ErrorBoundary, useErrorBoundary } from '@suspensive/react' import { useEffect } from 'react' import { fetchSomething } from './api' const SetErrorAfterFetch = () => { const errorBoundary = useErrorBoundary() useEffect(() => { fetchSomething().then( (response) => {}, (error) => errorBoundary.setError(error) ) }, []) return <>No error</> } export const Example = () => ( <ErrorBoundary fallback={({ error }) => <p>{error.message}</p>}> <SetErrorAfterFetch /> </ErrorBoundary> ) ``` `setError` routes async (event/effect) errors — which React error boundaries cannot catch via throw — into the nearest `ErrorBoundary`; `ErrorBoundary.Consumer` exposes the same object inline in JSX. ### Read error and reset inside a fallback without prop drilling ```tsx 'use client' import { ErrorBoundary, useErrorBoundaryFallbackProps } from '@suspensive/react' import { Feed } from './Feed' const ErrorBoundaryFallback = () => { const { error, reset } = useErrorBoundaryFallbackProps() return ( <div> <p>{error.message}</p> <button onClick={reset}>Try again</button> </div> ) } export const Example = () => ( <ErrorBoundary fallback={<ErrorBoundaryFallback />}> <Feed /> </ErrorBoundary> ) ``` ## Common Mistakes ### HIGH Using react-error-boundary prop interface on Suspensive ErrorBoundary Wrong: ```tsx import { ErrorBoundary } from '@suspensive/react' const Example = () => ( <ErrorBoundary FallbackComponent={ErrorFallback} onResetKeysChange={handleChange}> <Feed /> </ErrorBoundary> ) ``` Correct: ```tsx import { ErrorBoundary } from '@suspensive/react' const Example = () => ( <ErrorBoundary fallback={({ error, reset }) => <ErrorFallback error={error} onRetry={reset} />}> <Feed /> </ErrorBoundary> ) ``` Suspensive has a single unified `fallback` prop (no `FallbackComponent`/`fallbackRender`/`onResetKeysChange`), and reset is exposed as `reset` in fallback props, not `resetErrorBoundary`. Source: docs/suspensive.org/src/content/en/docs/react/comparison.mdx ### HIGH Expecting fallback errors to re-render the same fallback Wrong: ```tsx import { ErrorBoundary } from '@suspensive/react' // assuming this boundary catches its own fallback's errors forever const Example = ({ children }) => <ErrorBoundary fallback={<BrokenFallback />}>{children}</ErrorBoundary> ``` Correct: ```tsx import { ErrorBoundary } from '@suspensive/react' // wrap with a parent boundary to catch fallback errors const Example = ({ children }) => ( <ErrorBoundary fallback={<LastResort />}> <ErrorBoundary fallback={<BrokenFallback />}>{children}</ErrorBoundary> </ErrorBoundary> ) ``` Since v3, an error thrown inside a fallback propagates to the parent ErrorBoundary instead of recursively re-rendering the same fallback (the react-error-boundary and Suspensive v2 behavior) — agents trained on v2/react-error-boundary assume the old recursive behavior. Source: docs/suspensive.org/src/content/en/docs/react/migration/migrate-to-v3.mdx (#1409) ### HIGH Calling useErrorBoundaryFallbackProps outside a fallback Wrong: ```tsx import { ErrorBoundary, useErrorBoundaryFallbackProps } from '@suspensive/react' const Child = () => { const { reset } = useErrorBoundaryFallbackProps() // throws return <button onClick={reset}>retry</button> } const Example = () => ( <ErrorBoundary fallback={<Fallback />}> <Child /> </ErrorBoundary> ) ``` Correct: ```tsx import { ErrorBoundary, useErrorBoundaryFallbackProps } from '@suspensive/react' const Fallback = () => { const { error, reset } = useErrorBoundaryFallbackProps() return <button onClick={reset}>{error.message}</button> } const Example = ({ children }) => <ErrorBoundary fallback={<Fallback />}>{children}</ErrorBoundary> ``` The hook throws a runtime error when called in `children` or outside an `ErrorBoundary`; it is only valid inside the `fallback` element. Source: docs/suspensive.org/src/content/en/docs/react/ErrorBoundary.mdx (warning callout) ### HIGH Passing callback fallback from a Server Component Wrong: ```tsx // app/page.tsx — Server Component import { ErrorBoundary } from '@suspensive/react' export default function Page() { return ( <ErrorBoundary fallback={({ error }) => <p>{error.message}</p>}> <Content /> </ErrorBoundary> ) } ``` Correct: ```tsx // app/page.tsx — Server Component import { ErrorBoundary } from '@suspensive/react' import { ErrorFallbackClient } from './ErrorFallbackClient' export default function Page() { return ( <ErrorBoundary fallback={<ErrorFallbackClient />}> <Content /> </ErrorBoundary> ) } ``` ```tsx // ErrorFallbackClient.tsx 'use client' import { useErrorBoundaryFallbackProps } from '@suspensive/react' export const ErrorFallbackClient = () => { const { error, reset } = useErrorBoundaryFallbackProps() return <button onClick={reset}>{error.message}</button> } ``` In RSC, functions cannot cross the server-to-client boundary; pass JSX as `fallback` and read `error`/`reset` via `useErrorBoundaryFallbackProps` in a client component. Source: docs/suspensive.org/src/content/en/docs/react/ErrorBoundary.mdx (useErrorBoundaryFallbackProps section) ### HIGH Expecting ErrorBoundary to reset itself on remount Wrong: ```tsx import { ErrorBoundary } from '@suspensive/react' // navigating away and back, expecting the boundary to be clean const Example = ({ children }) => <ErrorBoundary fallback={<ErrorUI />}>{children}</ErrorBoundary> ``` Correct: ```tsx import { ErrorBoundary } from '@suspensive/react' import { useLocation } from 'react-router-dom' const Example = ({ children }) => { const location = useLocation() return ( <ErrorBoundary resetKeys={[location.key]} fallback={({ reset }) => <button onClick={reset}>retry</button>}> {children} </ErrorBoundary> ) } ``` A caught error persists across remounts by design — reset explicitly via the fallback's `reset`, `resetKeys`, or `ErrorBoundaryGroup`; when the error came from a query, the query error cache must be reset too (`onReset={useQueryErrorResetBoundary().reset}`). Source: https://github.com/toss/suspensive/issues/91 ### MEDIUM Manually filtering errors by rethrowing inside fallback Wrong: ```tsx import { ErrorBoundary } from '@suspensive/react' import { NetworkError } from './errors' const Example = ({ children }) => ( <ErrorBoundary fallback={({ error }) => { if (!(error instanceof NetworkError)) throw error return <NetworkErrorUI /> }} > {children} </ErrorBoundary> ) ``` Correct: ```tsx import { ErrorBoundary } from '@suspensive/react' import { NetworkError } from './errors' const Example = ({ children }) => ( <ErrorBoundary shouldCatch={NetworkError} fallback={<NetworkErrorUI />}> {children} </ErrorBoundary> ) ``` `shouldCatch` filters declaratively before the boundary catches, keeps TypeScript error narrowing, and avoids relying on fallback-rethrow semantics (which changed in v3). Source: docs/suspensive.org/src/content/en/docs/react/ErrorBoundary.mdx (shouldCatch section) ### MEDIUM Assuming caught values are always Error instances Wrong: ```tsx import { ErrorBoundary } from '@suspensive/react' const Example = ({ children }) => ( <ErrorBoundary fallback={({ error }) => <p>{error.message}</p>}>{children}</ErrorBoundary> ) ``` Correct: ```tsx import { ErrorBoundary } from '@suspensive/react' const Example = ({ children }) => ( <ErrorBoundary fallback={({ error }) => <p>{error instanceof Error ? error.message : String(error)}</p>}> {children} </ErrorBoundary> ) ``` Thrown strings and plain objects reach the fallback as-is, so fallback code assuming `error.message` renders `undefined` (or crashes) on non-Error throws. Source: https://github.com/toss/suspensive/issues/1785 See also: [migrate-to-v3](../../lifecycle/migrate-to-v3/SKILL.md) — v3 changed fallback error propagation semantics. See also: the `suspensive-react` composition skill in `@suspensive/react-query-4`/`@suspensive/react-query-5` — the removed `QueryErrorBoundary` replacement is `ErrorBoundary` + `useQueryErrorResetBoundary`, and `shouldCatch` layering applies to API error types.