error-boundary — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited error-boundary (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
An error boundary is React's recovery primitive for render-time failure: a component — still class-only as of React 19, because its defining methods (static getDerivedStateFromError, componentDidCatch) have no hooks equivalent — that catches JavaScript errors thrown anywhere in its descendant tree during rendering, lifecycle methods, and constructors, and renders a fallback UI instead of letting the throw unmount the broken subtree all the way to the root. It is a tree-level mechanism scoped by where you place it: a boundary near the page root catches everything but replaces the whole page on any error, while a boundary around a single widget catches only that widget's failure and lets the rest keep working — so placement is a granularity decision driven by failure-blast-radius, choosing the smallest scope where the fallback is still meaningful. Its defining limitation is the call-stack rule: it catches only what is thrown inside React's call stack during render or lifecycle, never event-handler throws, async/setTimeout/Promise rejections, SSR errors, or errors in the boundary itself — those must be caught locally and surfaced into render via setState to reach a boundary. It pairs with Suspense (a distinct primitive that catches thrown Promises rather than thrown Errors) and, to stay honest, must report every caught error to external observability so a fallback is the user-facing recovery and the developer-facing alarm rather than silent UI degradation.
The discipline of placing and configuring React error boundaries: precisely what the boundary catches (rendering errors, lifecycle method errors, constructor errors) and what it provably cannot catch (event handler errors, async/setTimeout/Promise errors, server-side rendering errors before React 18.5, errors thrown by the boundary itself), why React 19 still requires class-component implementation, the boundary-placement granularity tradeoff (page / feature / leaf), the canonical Suspense+ErrorBoundary nesting and why their order matters, the reset-and-recover pattern via resetKeys / FallbackComponent, the Next.js App Router conventions (error.tsx, global-error.tsx), and how to wire boundaries to external error reporting so caught errors do not become silent UI degradation.
Before React 16, an error thrown during render had nowhere to go. React's reconciler would catch it, log it, and continue trying to render — usually producing a corrupted or blank tree. The component model assumed that render functions don't throw, and the runtime had no recovery primitive when they did. The result was either total app crash or partial blank screens with no signal that anything had broken.
Error boundaries gave React the recovery primitive it had been missing. A class component that implements getDerivedStateFromError and/or componentDidCatch catches throws from anywhere in its descendant subtree during rendering, lifecycle methods, and constructors. When a throw occurs, React unmounts the broken subtree and renders the boundary's fallback in its place. The rest of the tree, outside the boundary, keeps running. A bug in one widget no longer means a blank page; it means that widget shows its fallback and everything else is fine.
The discipline is to place boundaries by failure-blast-radius. A boundary near the page root catches everything but replaces the entire page on any error. A boundary around a single widget catches only that widget's errors but lets the rest of the page keep working. The right granularity is the smallest scope where the fallback UI is still meaningful: a chart's error fallback ("Unable to load chart, retry?") is useful per-chart; a global "Something went wrong" replacing the whole app is useful only as a last resort.
The boundary's most important second-order property is honesty. A caught error is still an error. A boundary that swallows the throw without reporting externally degrades the UI silently — users see "Something went wrong," developers see nothing. The discipline is to pair every boundary with a report-to-Sentry (or equivalent) call, so the fallback is the user-facing recovery and the developer-facing alarm.
| Source of error | Caught by boundary? |
|---|---|
| Throw during a component's render | Yes |
Throw in componentDidMount / componentDidUpdate | Yes |
| Throw in a class constructor | Yes |
| Throw during hook execution (useState initializer, useEffect's render-phase setup) | Yes |
Throw in a useEffect callback body | No — runs after commit, outside render phase |
Throw in an event handler (onClick, onChange) | No — runs imperatively, outside render |
| Unhandled Promise rejection (async function thrown into a click handler) | No — async errors propagate to window.onunhandledrejection |
setTimeout / setInterval callback throw | No — escapes React's call stack |
Server-side rendering error (renderToString throw before React 18.5) | Partially — handled via onError in modern streaming APIs |
| Error thrown by the boundary itself | No — propagates to the next ancestor boundary |
The pattern: error boundaries catch errors thrown inside React's call stack during render or lifecycle. Errors that happen outside that stack — event handlers, async callbacks, promise rejections — must be handled with a local try/catch and then surfaced into React via setState (so the boundary catches the re-render with the error state).
Canonical async-error pattern:
function MyComponent() {
const [error, setError] = useState<Error | null>(null)
if (error) throw error // ← rethrow into render, so the boundary catches it
return (
<button onClick={async () => {
try {
await doRiskyThing()
} catch (e) {
setError(e as Error) // ← surface async error into state
}
}}>
Click
</button>
)
}This pattern is verbose but explicit. The boundary catches all rendering errors automatically; async errors must be opted-in by re-throwing during render.
Despite the rest of React moving to function components and hooks, error boundaries must be class components as of React 19. The reason: the boundary's defining methods are static getDerivedStateFromError and componentDidCatch, which have no hooks equivalent. The React team has discussed useErrorBoundary but it has not shipped.
Minimal class boundary:
import { Component, ReactNode } from 'react'
class ErrorBoundary extends Component<
{ fallback: ReactNode; children: ReactNode; onError?: (e: Error) => void },
{ hasError: boolean }
> {
state = { hasError: false }
static getDerivedStateFromError() {
return { hasError: true }
}
componentDidCatch(error: Error, info: React.ErrorInfo) {
this.props.onError?.(error)
// Report to Sentry/Datadog/etc here
}
render() {
return this.state.hasError ? this.props.fallback : this.props.children
}
}In practice, use react-error-boundary by Brian Vaughn / Kent C. Dodds — it provides <ErrorBoundary> with reset support, FallbackComponent, resetKeys, and onError / onReset props out of the box. Implementing the class yourself is fine, but react-error-boundary covers the cases (recovery, key-based reset) you'll eventually want.
Three placement strategies, each correct in different contexts:
1. App-root boundary. One boundary at the top, catches everything else's failures. Renders a "Something went wrong" generic page.
2. Route-segment boundary (Next.js error.tsx). One boundary per page or layout segment.
app/dashboard/error.tsx file is automatically a route-scoped boundary around app/dashboard/page.tsx. A global-error.tsx is the app-root backstop.3. Feature/widget boundary. A boundary around each independent feature or data-loading component.
The right approach is layered: app-root + route-segment + feature, with each catching what its layer scopes. A leaf widget's error hits the feature boundary; an unhandled error in the feature boundary hits the route boundary; a catastrophic error in the route boundary hits the app-root boundary.
Error boundaries catch thrown Errors. Suspense boundaries catch thrown Promises. They are distinct mechanisms and must be different components, but they almost always nest together:
<ErrorBoundary fallback={<ErrorUI />}>
<Suspense fallback={<LoadingUI />}>
<DataLoadingComponent />
</Suspense>
</ErrorBoundary>Order matters:
react-error-boundary provides onReset that pairs with Suspense's transition APIs to retry a failed fetch:
<ErrorBoundary
fallbackRender={({ resetErrorBoundary }) => (
<ErrorUI onRetry={resetErrorBoundary} />
)}
resetKeys={[queryId]}
>
<Suspense fallback={<Spinner />}>
<QueryResult queryId={queryId} />
</Suspense>
</ErrorBoundary>When queryId changes (e.g., user clicks a different item), the boundary auto-resets and tries again. When the user clicks Retry, resetErrorBoundary clears the error state and the Suspense boundary re-tries its async render.
See suspense-patterns for the full Suspense discipline; this skill focuses on the error half of the pair.
The App Router gives error boundaries first-class file conventions:
| File | Scope | What it catches |
|---|---|---|
app/<segment>/error.tsx | Route segment | Errors during rendering of that segment's page.tsx or any descendant Server/Client component; receives error and reset props |
app/global-error.tsx | Root layout fallback | Errors that escape every error.tsx — including errors in the root layout itself |
app/<segment>/not-found.tsx | Route segment | notFound() calls — special case of error, distinct UI |
error.tsx must be a Client Component (it uses state for the reset action) and is implicitly wrapped in a route-segment-level error boundary. The convention pairs with loading.tsx (Suspense fallback) and produces the canonical "fast page with skeleton, then content with error-recovery if something breaks" pattern without manual boundary wiring.
Important constraint: error.tsx only catches errors from its descendant tree, not from its sibling layout. An error in app/layout.tsx escapes the page's error.tsx and only global-error.tsx can catch it.
A caught error is still an error. Wire every boundary to external reporting:
<ErrorBoundary
FallbackComponent={ErrorFallback}
onError={(error, info) => {
Sentry.captureException(error, { contexts: { react: { componentStack: info.componentStack } } })
}}
>
<App />
</ErrorBoundary>info.componentStack is React's stack trace of which components were rendering when the error occurred — crucial for diagnosis, distinct from the JavaScript stack trace.
Sentry's @sentry/react provides a built-in <Sentry.ErrorBoundary> that handles reporting automatically. The discipline applies regardless of tool: caught errors must surface somewhere developers see them. A boundary without reporting is a UX feature with no signal-back loop. See error-tracking for the broader observability discipline this skill plugs into.
| Anti-pattern | Why it's wrong | Fix |
|---|---|---|
| Single app-root boundary, no finer scopes | Any error blanks the whole app | Layer boundaries: app-root + route-segment + feature |
| Boundary that swallows the error without reporting | Silent UI degradation | Wire onError / componentDidCatch to Sentry or equivalent |
| Trying to catch event-handler errors with a boundary | Boundary doesn't catch async/imperative errors | Local try/catch in the handler, surface to state via setError, re-throw during render |
| Boundary at the same level as the failing component | A throw during the boundary's own render escapes the boundary | Boundary must be above the failing component in the tree |
error.tsx placed in the wrong route segment | Errors in app/layout.tsx aren't caught by app/page/error.tsx | Place error.tsx at the segment that owns the failing render; use global-error.tsx for the root layout |
| ErrorBoundary inside Suspense (when goal is "failure replaces loading state") | Loading state lingers after fetch fails | Put ErrorBoundary outside Suspense for the common case |
| Function-component error boundary attempt | Hooks API has no useErrorBoundary (as of React 19) | Use a class component or the react-error-boundary library |
| Reset that doesn't actually retry the failed work | User clicks "Retry" but the same stale error state persists | Use resetKeys tied to the inputs that should retry, or resetErrorBoundary from react-error-boundary |
After applying this skill, verify:
error.tsx (or equivalent boundary).onError / componentDidCatch that reports to external observability.resetKeys are tied to inputs that change on retry.global-error.tsx exists as the app-root backstop.getDerivedStateFromError and componentDidCatch.react-error-boundary. The de facto library; reset semantics and FallbackComponent API.error.tsx and global-error.tsx. Route-segment boundary conventions.| Instead of this skill | Use | Why |
|---|---|---|
| Suspense boundary placement (loading states, streaming) | suspense-patterns | suspense-patterns owns the loading-state mechanism; this skill owns the failure-state mechanism. They pair but are distinct primitives. |
| Setting up the observability SDK (Sentry init, breadcrumbs, sampling) | error-tracking | error-tracking owns the SDK and infrastructure; this skill owns the in-tree React boundary that feeds caught errors into the SDK. |
| Try/catch discipline in async code (event handlers, promises, await) | code-review (or general async error patterns) | Async errors are outside React's call stack — they need local handling first, then re-throwing into render to reach the boundary. |
| Backend error response design (HTTP status codes, error envelope format) | api-design | api-design owns the wire format for backend errors; this skill handles client-side errors after the response. |
| Server Component error handling (server-side render failure) | server-components-design | Server-side rendering errors are caught by error.tsx files at the route segment level — a Next.js convention, technically distinct from React's render-phase error boundary. |
<!-- skill-graph-context:start (generated — do not edit by hand) -->
Classification
frontend-engineeringtrueengineering/frontendWhen to use
my error boundary isn't catching errors, do I need an error boundary here, why does the whole page crash on one component error, how do I recover from a caught error, error.tsx vs global-error.tsx, why doesn't this catch async errorsNot for
suspense-patterns: the loading-state boundary mechanismhooks-patternsRelated skills
code-review, error-trackingsuspense-patterns, hooks-patterns, error-tracking, server-components-designConcept
Keywords
React Error Boundary, componentDidCatch, getDerivedStateFromError, react-error-boundary library, Next.js error.tsx, global-error.tsx, error boundary granularity, resetKeys, error boundary with Suspense, caught render error<!-- skill-graph-context:end -->
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.