Skip to content

feat(react): add ErrorBoundary.Observer#1911

Open
manudeli wants to merge 5 commits intomainfrom
react/feat/ErrorBoundary.Observer
Open

feat(react): add ErrorBoundary.Observer#1911
manudeli wants to merge 5 commits intomainfrom
react/feat/ErrorBoundary.Observer

Conversation

@manudeli
Copy link
Copy Markdown
Member

@manudeli manudeli commented Mar 5, 2026

new ErrorBoundary.Observer

ErrorBoundary.Observer is a component that observes errors from all nested <ErrorBoundary/> components. It does not catch or handle errors — it simply observes them, making it ideal for integrating with external error reporting tools like Sentry.

Motivation

Previously, you had to attach onError prop individually to each <ErrorBoundary/>. To observe errors across the entire app, you had to repeatedly pass the same handler to every ErrorBoundary.

AS-IS

import { ErrorBoundary } from '@suspensive/react'
import * as Sentry from '@sentry/react'

function App() {
  return (
    <ErrorBoundary
      fallback={<FallbackA />}
      onError={Sentry.captureReactException} // repeated every time
    >
      <ErrorBoundary
        fallback={<FallbackB />}
        onError={Sentry.captureReactException} // repeated every time
      >
        <YourComponent />
      </ErrorBoundary>
    </ErrorBoundary>
  )
}

TO-BE

import { ErrorBoundary } from '@suspensive/react'
import * as Sentry from '@sentry/react'

function App() {
  return (
    <ErrorBoundary.Observer onError={Sentry.captureReactException}>
      <ErrorBoundary fallback={<FallbackA />}>
        <ErrorBoundary fallback={<FallbackB />}>
          <YourComponent />
        </ErrorBoundary>
      </ErrorBoundary>
    </ErrorBoundary.Observer>
  )
}

Additive (not override)

ErrorBoundary.Observer does not override each <ErrorBoundary/>'s own onError. Both are always called.

import { ErrorBoundary } from '@suspensive/react'
import * as Sentry from '@sentry/react'

function App() {
  return (
    <ErrorBoundary.Observer onError={Sentry.captureReactException}>
      <ErrorBoundary
        fallback={<FallbackA />}
        onError={(error) => console.log('also called!', error)} // not overridden by Observer
      >
        <YourComponent />
      </ErrorBoundary>
    </ErrorBoundary.Observer>
  )
}
// When an error occurs:
// 1. ErrorBoundary's onError: console.log('also called!', error)
// 2. ErrorBoundary.Observer's onError: Sentry.captureReactException(error, info)

Features

  • Bubble order: Execution order is local onError → inner Observer → outer Observer
  • Nestable: Multiple ErrorBoundary.Observer components can be nested, and all will be called
  • @experimental: Marked as an experimental feature

Changes

  • packages/react/src/ErrorBoundary.tsx: Add ErrorBoundary.Observer static member and ErrorObserverContext
  • packages/react/src/ErrorBoundary.spec.tsx: Add 4 test cases
  • docs/suspensive.org/src/content/{en,ko}/docs/react/ErrorBoundary.mdx: Add documentation (comparison table, Observer section, Sentry migration guide)

…ror handling

- Added `ErrorBoundary.Observer` component to capture errors from all nested `<ErrorBoundary/>` components.
- Updated error handling to call both the Observer's `onError` and each ErrorBoundary's `onError` when an error occurs.
- Enhanced documentation and examples for using `ErrorBoundary.Observer`.
- Added tests to verify the functionality of the new Observer component and its interaction with nested ErrorBoundaries.
@coauthors
Copy link
Copy Markdown

coauthors bot commented Mar 5, 2026

People can be co-author:

Candidate Reasons Count Add this as commit message
@manudeli #1911 (comment) #1911 (review) #1911 (comment) #1911 4 Co-authored-by: manudeli <61593290+manudeli@users.noreply.github.com>
@Copilot #1911 (comment) #1911 (comment) #1911 (comment) 3 Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@gwansikk #1911 (review) #1911 (comment) 2 Co-authored-by: gwansikk <39869096+gwansikk@users.noreply.github.com>
@codecov-commenter #1911 (comment) 1 Co-authored-by: codecov-commenter <65553080+codecov-commenter@users.noreply.github.com>
@kangju2000 #1911 (review) 1 Co-authored-by: kangju2000 <23312485+kangju2000@users.noreply.github.com>

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 5, 2026

🦋 Changeset detected

Latest commit: fc912bf

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 6 packages
Name Type
@suspensive/react Minor
@suspensive/react-query Minor
@suspensive/react-query-4 Minor
@suspensive/react-query-5 Minor
@suspensive/jotai Minor
@suspensive/codemods Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Mar 5, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
suspensive-next-streaming-react-query Ready Ready Preview, Comment Mar 27, 2026 3:53pm
v2.suspensive.org Ready Ready Preview, Comment Mar 27, 2026 3:53pm
v3.suspensive.org Ready Ready Preview, Comment Mar 27, 2026 3:53pm
visualization.suspensive.org Ready Ready Preview, Comment Mar 27, 2026 3:53pm

Request Review

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 5, 2026

Size Change: +276 B (+0.46%)

Total Size: 60.3 kB

Filename Size Change
packages/react/dist/ErrorBoundary.cjs 2.24 kB +137 B (+6.51%) 🔍
packages/react/dist/ErrorBoundary.mjs 2.18 kB +139 B (+6.82%) 🔍
ℹ️ View Unchanged
Filename Size
packages/jotai/dist/Atom.cjs 317 B
packages/jotai/dist/Atom.mjs 249 B
packages/jotai/dist/AtomValue.cjs 305 B
packages/jotai/dist/AtomValue.mjs 236 B
packages/jotai/dist/index.cjs 174 B
packages/jotai/dist/index.mjs 88 B
packages/jotai/dist/SetAtom.cjs 303 B
packages/jotai/dist/SetAtom.mjs 234 B
packages/next/dist/index.cjs 305 B
packages/next/dist/index.mjs 250 B
packages/next/dist/react-******.cjs 268 B
packages/next/dist/react-******.mjs 213 B
packages/react-query-4/dist/createGetQueryClient.cjs 1.09 kB
packages/react-query-4/dist/createGetQueryClient.mjs 1.01 kB
packages/react-query-4/dist/index.cjs 446 B
packages/react-query-4/dist/index.mjs 298 B
packages/react-query-4/dist/infiniteQueryOptions.cjs 357 B
packages/react-query-4/dist/infiniteQueryOptions.mjs 292 B
packages/react-query-4/dist/IsFetching.cjs 491 B
packages/react-query-4/dist/IsFetching.mjs 413 B
packages/react-query-4/dist/Mutation.cjs 412 B
packages/react-query-4/dist/Mutation.mjs 332 B
packages/react-query-4/dist/mutationOptions.cjs 187 B
packages/react-query-4/dist/mutationOptions.mjs 134 B
packages/react-query-4/dist/PrefetchInfiniteQuery.cjs 463 B
packages/react-query-4/dist/PrefetchInfiniteQuery.mjs 387 B
packages/react-query-4/dist/PrefetchQuery.cjs 453 B
packages/react-query-4/dist/PrefetchQuery.mjs 379 B
packages/react-query-4/dist/QueriesHydration.cjs 1.66 kB
packages/react-query-4/dist/QueriesHydration.mjs 1.56 kB
packages/react-query-4/dist/QueryClientConsumer.cjs 353 B
packages/react-query-4/dist/QueryClientConsumer.mjs 277 B
packages/react-query-4/dist/queryOptions.cjs 353 B
packages/react-query-4/dist/queryOptions.mjs 286 B
packages/react-query-4/dist/SuspenseInfiniteQuery.cjs 668 B
packages/react-query-4/dist/SuspenseInfiniteQuery.mjs 580 B
packages/react-query-4/dist/SuspenseQueries.cjs 569 B
packages/react-query-4/dist/SuspenseQueries.mjs 483 B
packages/react-query-4/dist/SuspenseQuery.cjs 654 B
packages/react-query-4/dist/SuspenseQuery.mjs 568 B
packages/react-query-4/dist/usePrefetchInfiniteQuery.cjs 462 B
packages/react-query-4/dist/usePrefetchInfiniteQuery.mjs 395 B
packages/react-query-4/dist/usePrefetchQuery.cjs 452 B
packages/react-query-4/dist/usePrefetchQuery.mjs 388 B
packages/react-query-4/dist/useSuspenseInfiniteQuery.cjs 375 B
packages/react-query-4/dist/useSuspenseInfiniteQuery.mjs 305 B
packages/react-query-4/dist/useSuspenseQueries.cjs 368 B
packages/react-query-4/dist/useSuspenseQueries.mjs 299 B
packages/react-query-4/dist/useSuspenseQuery.cjs 365 B
packages/react-query-4/dist/useSuspenseQuery.mjs 298 B
packages/react-query-5/dist/createGetQueryClient.cjs 1.09 kB
packages/react-query-5/dist/createGetQueryClient.mjs 1.01 kB
packages/react-query-5/dist/index.cjs 438 B
packages/react-query-5/dist/index.mjs 294 B
packages/react-query-5/dist/infiniteQueryOptions.cjs 352 B
packages/react-query-5/dist/infiniteQueryOptions.mjs 286 B
packages/react-query-5/dist/IsFetching.cjs 445 B
packages/react-query-5/dist/IsFetching.mjs 366 B
packages/react-query-5/dist/Mutation.cjs 412 B
packages/react-query-5/dist/Mutation.mjs 332 B
packages/react-query-5/dist/mutationOptions.cjs 350 B
packages/react-query-5/dist/mutationOptions.mjs 284 B
packages/react-query-5/dist/PrefetchInfiniteQuery.cjs 466 B
packages/react-query-5/dist/PrefetchInfiniteQuery.mjs 391 B
packages/react-query-5/dist/PrefetchQuery.cjs 459 B
packages/react-query-5/dist/PrefetchQuery.mjs 383 B
packages/react-query-5/dist/QueriesHydration.cjs 1.66 kB
packages/react-query-5/dist/QueriesHydration.mjs 1.56 kB
packages/react-query-5/dist/QueryClientConsumer.cjs 351 B
packages/react-query-5/dist/QueryClientConsumer.mjs 276 B
packages/react-query-5/dist/queryOptions.cjs 347 B
packages/react-query-5/dist/queryOptions.mjs 281 B
packages/react-query-5/dist/SuspenseInfiniteQuery.cjs 668 B
packages/react-query-5/dist/SuspenseInfiniteQuery.mjs 580 B
packages/react-query-5/dist/SuspenseQueries.cjs 585 B
packages/react-query-5/dist/SuspenseQueries.mjs 498 B
packages/react-query-5/dist/SuspenseQuery.cjs 645 B
packages/react-query-5/dist/SuspenseQuery.mjs 558 B
packages/react-query-5/dist/usePrefetchInfiniteQuery.cjs 367 B
packages/react-query-5/dist/usePrefetchInfiniteQuery.mjs 300 B
packages/react-query-5/dist/usePrefetchQuery.cjs 364 B
packages/react-query-5/dist/usePrefetchQuery.mjs 294 B
packages/react-query-5/dist/useSuspenseInfiniteQuery.cjs 368 B
packages/react-query-5/dist/useSuspenseInfiniteQuery.mjs 299 B
packages/react-query-5/dist/useSuspenseQueries.cjs 363 B
packages/react-query-5/dist/useSuspenseQueries.mjs 294 B
packages/react-query-5/dist/useSuspenseQuery.cjs 359 B
packages/react-query-5/dist/useSuspenseQuery.mjs 292 B
packages/react-query/dist/index.cjs 383 B
packages/react-query/dist/index.mjs 201 B
packages/react-query/dist/v4.cjs 383 B
packages/react-query/dist/v4.mjs 201 B
packages/react-query/dist/v5.cjs 383 B
packages/react-query/dist/v5.mjs 201 B
packages/react/dist/ClientOnly.cjs 606 B
packages/react/dist/ClientOnly.mjs 536 B
packages/react/dist/DefaultProps.cjs 968 B
packages/react/dist/DefaultProps.mjs 901 B
packages/react/dist/Delay.cjs 985 B
packages/react/dist/Delay.mjs 906 B
packages/react/dist/ErrorBoundaryGroup.cjs 1.11 kB
packages/react/dist/ErrorBoundaryGroup.mjs 1.04 kB
packages/react/dist/index.cjs 342 B
packages/react/dist/index.mjs 229 B
packages/react/dist/lazy.cjs 2.04 kB
packages/react/dist/lazy.mjs 1.98 kB
packages/react/dist/Suspense.cjs 801 B
packages/react/dist/Suspense.mjs 716 B
packages/react/dist/useIsClient.cjs 295 B
packages/react/dist/useIsClient.mjs 233 B

compressed-size-action

…h usage examples

- Updated documentation for `ErrorBoundary.Observer` to clarify its purpose in observing errors from nested `<ErrorBoundary/>` components.
- Added before-and-after code examples demonstrating the simplification of error handling with `ErrorBoundary.Observer`.
- Improved explanations of the `onError` prop usage in the context of nested error boundaries.
@manudeli manudeli changed the title feat(ErrorBoundary): introduce ErrorBoundary.Observer for enhanced er… feat(react): add ErrorBoundary.Observer Mar 5, 2026
Comment thread .changeset/early-hornets-relax.md Outdated
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new ErrorBoundary.Observer component to @suspensive/react that lets consumers observe onError events from all nested <ErrorBoundary /> instances, primarily for centralized error reporting integrations (e.g., Sentry).

Changes:

  • Add ErrorBoundary.Observer (via a new internal ErrorObserverContext) and wire ErrorBoundary’s onError to also notify the observer chain.
  • Add unit tests covering observer invocation and bubbling order.
  • Update English/Korean docs and add a changeset for a minor release.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/react/src/ErrorBoundary.tsx Implements ErrorBoundary.Observer and context-based bubbling of onError notifications.
packages/react/src/ErrorBoundary.spec.tsx Adds test cases validating observer callbacks and call order.
docs/suspensive.org/src/content/ko/docs/react/ErrorBoundary.mdx Documents ErrorBoundary.Observer, updates comparison table and Sentry migration guidance (KO).
docs/suspensive.org/src/content/en/docs/react/ErrorBoundary.mdx Documents ErrorBoundary.Observer, updates comparison table and Sentry migration guidance (EN).
.changeset/early-hornets-relax.md Declares a minor version bump for @suspensive/react for the new feature.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

*
* @example
* ```tsx
* import * as Sentry from '@sentry/react'
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JSDoc example uses ErrorBoundary.Observer/<ErrorBoundary/> but only imports @sentry/react. This snippet won’t type-check/run as written; it should also import ErrorBoundary from @suspensive/react (and/or adjust the example to be self-contained).

Suggested change
* import * as Sentry from '@sentry/react'
* import * as Sentry from '@sentry/react'
* import { ErrorBoundary } from '@suspensive/react'

Copilot uses AI. Check for mistakes.
Comment on lines +279 to +280
* A component that captures errors from all nested ErrorBoundary components.
* Both Observer's onError and each ErrorBoundary's own onError will be called when an error is caught.
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring says this component “captures” errors, but ErrorBoundary.Observer doesn’t catch/handle errors (it only observes callbacks from nested ErrorBoundarys). Consider rephrasing to avoid implying it behaves like an error boundary itself.

Suggested change
* A component that captures errors from all nested ErrorBoundary components.
* Both Observer's onError and each ErrorBoundary's own onError will be called when an error is caught.
* A component that observes errors reported by all nested ErrorBoundary components.
* It does not catch errors itself, but its onError callback is invoked whenever a nested ErrorBoundary catches an error,
* in addition to that ErrorBoundary's own onError handler.

Copilot uses AI. Check for mistakes.
Comment on lines +300 to +302
Observer: ({ onError, children }: PropsWithChildren<{ onError: (error: Error, info: ErrorInfo) => void }>) => {
const parent = useContext(ErrorObserverContext)
const handleError = useCallback(
Copy link

Copilot AI Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This introduces a new public API surface (ErrorBoundary.Observer), but there are no corresponding type-level tests in ErrorBoundary.test-d.tsx (the repo already uses .test-d.tsx for public typings). Adding a minimal d.ts test for JSX usage and the onError signature would help prevent accidental type regressions.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

@kangju2000 kangju2000 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1. The name "Observer" might not align with the actual behavior

"Observer" tends to evoke the observer pattern (subscribe/unsubscribe), but the actual implementation is a Context Provider — it propagates an error callback through Context for ErrorBoundary to consume. There might be a gap between the mental model the name suggests and how it actually works.

2. Could a wrapper component achieve a similar result?

If the core motivation is "apply the same onError to all ErrorBoundaries," a wrapper seems like it could solve this just as well:

const AppErrorBoundary = (props) => (
  <ErrorBoundary onError={Sentry.captureReactException} {...props} />
)

Observer adds useContext + useCallback to every ErrorBoundary instance for this purpose, and this cost is incurred even when Observer isn't used — I'm curious whether this tradeoff is considered acceptable.

3. If the goal is default props, would DefaultPropsProvider be a more natural fit?

Since suspensive already has DefaultPropsProvider, adding a default onError there might feel more consistent with the existing API. Extending a pattern users already know could be preferable to introducing a new component — what do you think?

@manudeli
Copy link
Copy Markdown
Member Author

manudeli commented Mar 5, 2026

1. The name "Observer" might not align with the actual behavior

"Observer" tends to evoke the observer pattern (subscribe/unsubscribe), but the actual implementation is a Context Provider — it propagates an error callback through Context for ErrorBoundary to consume. There might be a gap between the mental model the name suggests and how it actually works.

I agree. I want to find another candidate for naming.

  1. ErrorBoundary.ErrorCapture
  2. ErrorBoundary.OnError
  3. ErrorBoundary.ErrorReporter
  4. ErrorBoundary.ErrorListener
  5. ErrorBoundary.ErrorSink
  6. ErrorBoundary.ErrorMonitor

2. Could a wrapper component achieve a similar result?

If the core motivation is "apply the same onError to all ErrorBoundaries," a wrapper seems like it could solve this just as well:

const AppErrorBoundary = (props) => (
  <ErrorBoundary onError={Sentry.captureReactException} {...props} />
)

Observer adds useContext + useCallback to every ErrorBoundary instance for this purpose, and this cost is incurred even when Observer isn't used — I'm curious whether this tradeoff is considered acceptable.

3. If the goal is default props, would DefaultPropsProvider be a more natural fit?

Since suspensive already has DefaultPropsProvider, adding a default onError there might feel more consistent with the existing API. Extending a pattern users already know could be preferable to introducing a new component — what do you think?

Default prop should be available to be overridden.
If the component wrapping suspensive ErrorBoundary should get type narrowing like the suspensive ErrorBoundary, that would be quite hard because there are generic types

@codecov-commenter
Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.35%. Comparing base (71d6218) to head (fc912bf).

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #1911      +/-   ##
==========================================
+ Coverage   93.22%   93.35%   +0.13%     
==========================================
  Files          42       42              
  Lines         664      677      +13     
  Branches      164      164              
==========================================
+ Hits          619      632      +13     
  Misses         42       42              
  Partials        3        3              
Components Coverage Δ
@suspensive/react 100.00% <100.00%> (ø)
@suspensive/react-query 95.83% <ø> (ø)
@suspensive/react-query-4 100.00% <ø> (ø)
@suspensive/react-query-5 100.00% <ø> (ø)
@suspensive/jotai 100.00% <ø> (ø)
@suspensive/codemods 81.60% <ø> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@gwansikk
Copy link
Copy Markdown
Collaborator

I think this API is similar to TanStack Form's listeners in what it aims to solve — handling side-effects in addition to the actual action (local onError).

What do you think about defining it as ErrorBoundary.Listeners, framing it as an API specifically for handling side-effects of ErrorBoundary?

ref: https://tanstack.com/form/v1/docs/framework/react/guides/listeners


  1. Could a wrapper component achieve a similar result?

My thought on this — if we support the API with a side-effect-oriented approach, it could be handled more elegantly. For example, think about bubble order. On top of that, allowing propagation to be stopped (e.g. e.stopPropagation()) would give even more flexibility in managing the effects of error boundaries.

Copy link
Copy Markdown
Collaborator

@gwansikk gwansikk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants