Skip to content

feat(core): Capture Expo Router ErrorBoundary errors#6318

Draft
alwx wants to merge 1 commit into
mainfrom
feat/expo-router-error-boundary
Draft

feat(core): Capture Expo Router ErrorBoundary errors#6318
alwx wants to merge 1 commit into
mainfrom
feat/expo-router-error-boundary

Conversation

@alwx

@alwx alwx commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

📢 Type of change

  • New feature

📜 Description

Adds Sentry.wrapRouterErrorBoundary — wraps Expo Router's per-route ErrorBoundary so render-phase errors that hit the fallback are captured with route context (route.name, route.path, route.params), the active navigation transaction is marked errored, and a breadcrumb is emitted. Concrete path/params are gated behind sendDefaultPii.

💡 Motivation and Context

Closes #6160.

💚 How did you test it?

New unit tests in expoRouterErrorBoundary.test.tsx. Full suite green locally.

📝 Checklist

  • I added tests to verify changes
  • No new PII added or SDK only sends newly added PII if sendDefaultPII is enabled
  • I updated the docs if needed.
  • I updated the wizard if needed.
  • All tests passing
  • No breaking changes

🔮 Next steps

Investigate Metro/Babel auto-instrumentation of export { ErrorBoundary } from 'expo-router' re-exports as a follow-up.

@github-actions

github-actions Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Semver Impact of This PR

None (no version bump detected)

📋 Changelog Preview

This is how your changes will appear in the changelog.
Entries from this PR are highlighted with a left border (blockquote style).


  • feat(core): Capture Expo Router ErrorBoundary errors by alwx in #6318
  • chore(deps): update Sentry Android Gradle Plugin to v6.12.0 by github-actions in #6317
  • chore(deps): update Cocoa SDK to v9.18.0 by github-actions in #6311
  • fix(android): Prevent breadcrumb loss from numeric timestamp ClassCastException by antonis in #6308
  • chore(deps): update Android SDK to v8.44.0 by github-actions in #6310
  • chore(deps): update CLI to v3.5.1 by github-actions in #6305
  • chore(deps): update JavaScript SDK to v10.58.0 by github-actions in #6296
  • chore: Fix lint issues by antonis in #6300
  • chore: Bump sample and perf test apps to React Native 0.86.0 by antonis in #6287
  • fix(deps): bump form-data from 4.0.5 to 4.0.6 by antonis in #6297
  • fix(ci): Handle @sentry-internal/* package renames in JS updater by antonis in #6295
  • Record network request/response bodies in Session Replay by alwx in #6288
  • chore(deps): bump tar from 7.5.11 to 7.5.16 by dependabot in #6293
  • fix(ci): Update renamed @sentry-internal/* packages in JS updater script by antonis in #6294
  • chore(deps): bump launch-editor from 2.11.1 to 2.14.1 by dependabot in #6291
  • chore(deps-dev): bump @babel/core from 7.26.7 to 7.29.6 by dependabot in #6292
  • fix(deps): Resolve shell-quote to >=1.8.4 (Dependabot RNSentryModule.captureEvent is ignoring environment #547) by antonis in #6286
  • fix(ci): Support version catalog in android SDK version check by antonis in #6280
  • test(e2e): Bump E2E tests to React Native 0.86.0 by antonis in #6268
  • feat(android): Add nativeStackAndroid support to NativeLinkedErrors by lucas-zimerman in #6278
  • chore(deps): bump ruby/setup-ruby from 1.310.0 to 1.313.0 by dependabot in #6282
  • chore(deps): update Maestro to v2.6.1 by github-actions in #6277
  • chore(deps): bump gradle/actions from 6.1.0 to 6.2.0 by dependabot in #6284
  • chore(deps): bump getsentry/craft from 2.26.8 to 2.26.10 by dependabot in #6283

Plus 7 more


🤖 This preview updates automatically when you update the PR.

Add `Sentry.wrapRouterErrorBoundary` to wrap Expo Router's per-route
`ErrorBoundary` so render-phase errors that hit the fallback are
captured with route context, the active navigation transaction is
marked errored, and a breadcrumb is emitted.

Closes #6160
@alwx alwx force-pushed the feat/expo-router-error-boundary branch from e252cc5 to f313648 Compare June 18, 2026 12:38
@github-actions

Copy link
Copy Markdown
Contributor
Fails
🚫 Pull request is not ready for merge, please add the "ready-to-merge" label to the pull request
🚫 Please consider adding a changelog entry for the next release.
Messages
📖 Do not forget to update Sentry-docs with your feature once the pull request gets approved.

Instructions and example for changelog

Please add an entry to CHANGELOG.md to the "Unreleased" section. Make sure the entry includes this PR's number.

Example:

## Unreleased

### Features

- Capture Expo Router ErrorBoundary errors ([#6318](https://github.com/getsentry/sentry-react-native/pull/6318))

If none of the above apply, you can opt out of this check by adding #skip-changelog to the PR description or adding a skip-changelog label.

Generated by 🚫 dangerJS against f313648

@sentry-warden sentry-warden Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Sentry capture fires during render phase, risking duplicate events or missed deduplication on remount

Calling reportRouterBoundaryError in the render body rather than in a useEffect means the capture can fire during an abandoned Concurrent Mode render (error reported but boundary never committed) and will re-fire after any genuine unmount+remount because useRef resets on remount — the ref-based dedup only prevents duplicates within the same component lifetime. Consider moving the logic to useEffect(() => { if (props.error && reportedErrorRef.current !== props.error) { reportedErrorRef.current = props.error; reportRouterBoundaryError(props.error); } }, [props.error]).

Evidence
  • expoRouterErrorBoundary.tsx lines 56-58: the guard and captureException call are in the function body, not inside any useEffect.
  • useRef is initialized to null on every fresh mount; when Expo Router or a parent unmounts and remounts the boundary (e.g., navigation stack pop+push with the same error prop), a new ref starts at null, bypassing the dedup check and emitting a second event for the same error.
  • In React 18 Concurrent Mode, renders that are interrupted and abandoned will still have executed lines 57-58, reporting to Sentry before the fallback UI ever commits.
  • The test suite (expoRouterErrorBoundary.test.tsx) only covers same-instance rerenders and does not exercise the unmount→remount path, so this scenario is untested.

Identified by Warden code-review

Comment on lines +57 to +60
reportedErrorRef.current = props.error;
reportRouterBoundaryError(props.error);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Side effects (captureException, addBreadcrumb, span mutation) executed during React render phase

Calling reportRouterBoundaryError synchronously inside the render body violates React's rule that renders must be pure; in React 18 concurrent mode, renders can be interrupted and the ref mutation on line 58 will suppress the deduplication guard without the error ever being reported after commit. Move to a useEffect(() => { … }, [props.error]) to tie the side effect to the commit phase.

Evidence
  • reportRouterBoundaryError calls addBreadcrumb, captureException, and root.setStatus — all observable external side effects.
  • reportedErrorRef.current = props.error (line 58) mutates a ref during render, which React explicitly warns against in concurrent mode since the same render may run multiple times before committing.
  • If React discards the in-progress render (e.g. due to a higher-priority update), the ref is already set to the error value so the guard on line 57 will prevent a second attempt on the retry render, silently dropping the capture.
  • The tests invoke render(...) synchronously without Strict Mode wrapping, so this double-invoke / discard scenario is not exercised.
  • The correct pattern is useEffect(() => { if (props.error && reportedErrorRef.current !== props.error) { reportedErrorRef.current = props.error; reportRouterBoundaryError(props.error); } }, [props.error]);
Also found at 1 additional location
  • packages/core/src/js/index.ts:136

Identified by Warden code-review · JQW-LSF

@alwx alwx marked this pull request as ready for review June 18, 2026 13:10
@alwx alwx marked this pull request as draft June 18, 2026 13:10
@alwx

alwx commented Jun 18, 2026

Copy link
Copy Markdown
Contributor Author

it's still a draft, don't review yet

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit f313648. Configure here.

return event;
});
return scope;
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Sentry failure blocks error UI

High Severity

reportRouterBoundaryError runs synchronously during the wrapped boundary’s render, before OriginalErrorBoundary is returned. Nothing wraps addBreadcrumb, captureException, or span status updates, so any thrown Sentry instrumentation error aborts render and can prevent Expo Router’s fallback UI from showing.

Fix in Cursor Fix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

Reviewed by Cursor Bugbot for commit f313648. Configure here.

@divineniiquaye

Copy link
Copy Markdown

Really needed this, looking forward for it been merged

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Surface Expo Router's ErrorBoundary and route-level errors

2 participants