feat(react): Add `Sentry.reactErrorHandler`
ref https://github.com/getsentry/sentry-javascript/issues/11798
React 19 brings some improvements to error handling with https://react.dev/blog/2024/04/25/react-19#error-handling! This PR exposes a new method, Sentry.captureReactException that helps users interact with these new React 19 error handling hooks.
Background
React Error handling originally relied only on error boundaries, React components that would automatically catch component rendering errors and display fallback UIs.
class ErrorBoundary extends React.Component {
// ...
public componentDidCatch(error: any, info: React.ErrorInfo): void {
// Example "componentStack":
// in ComponentThatThrows (created by App)
// in ErrorBoundary (created by App)
// in div (created by App)
// in App
logErrorToMyService(error, info.componentStack);
}
}
We added support for this by exporting our own ErrorBoundary component.
If you notice above, the info parameter passed into componentDidCatch has a componentStack property. componentStack is a synthetic stacktrace that represents the component call-chain that threw the error. Fortunately because this is a synthetic stacktrace, we can parse it as an error in the SDK and attach it to the original exception as a linked exception via error.cause.
Important to note: If a component chain is not wrapped in an error boundary though, we do not get access to the componentStack, as the error bubbles up to the global error handler.
React 19
In React 19 they've two new options, onCaughtError and onUncaughtError to the React DOM public API.
onCaughtError is a callback called when React catches an error in an Error Boundary. This effectively works just like having an ErrorBoundary with componentDidCatch.
onUncaughtError is a callback called when an error is thrown and not caught by an Error Boundary. This means we can add componentStack information to these errors without requiring users to add error boundaries everywhere (useful for 3rd party component libraries and similar).
Given onCaughtError, onUncaughtError and componentDidCatch all have identical APIs
declare function componentDidCatch(error: any, info: React.ErrorInfo): void;
This PR introduces Sentry.reactErrorHandler, which looks like so:
import * as Sentry from '@sentry/react';
import { hydrateRoot } from "react-dom/client";
ReactDOM.hydrateRoot(
document.getElementById("root") as HTMLElement,
<React.StrictMode>
<App />
</React.StrictMode>,
{
onUncaughtError: Sentry.reactErrorHandler(),
onCaughtError: Sentry.reactErrorHandler((error, errorInfo) => {
// optional callback if users want custom config.
}),
}
);
To validate this change, we add a react 19 e2e test.
Next Steps
After we merge this in and update the docs, we can explore automatically instrumenting createRoot from react-dom to add Sentry.reactErrorHandler accordingly, but that is a next step.
size-limit report 📦
| Path | Size |
|---|---|
| @sentry/browser | 21.78 KB (0%) |
| @sentry/browser (incl. Tracing) | 32.79 KB (-0.01% 🔽) |
| @sentry/browser (incl. Tracing, Replay) | 68.26 KB (0%) |
| @sentry/browser (incl. Tracing, Replay) - with treeshaking flags | 61.68 KB (+0.01% 🔺) |
| @sentry/browser (incl. Tracing, Replay with Canvas) | 72.31 KB (0%) |
| @sentry/browser (incl. Tracing, Replay, Feedback) | 84.37 KB (0%) |
| @sentry/browser (incl. Tracing, Replay, Feedback, metrics) | 85.84 KB (0%) |
| @sentry/browser (incl. metrics) | 23.17 KB (0%) |
| @sentry/browser (incl. Feedback) | 37.8 KB (0%) |
| @sentry/browser (incl. sendFeedback) | 26.36 KB (0%) |
| @sentry/browser (incl. FeedbackAsync) | 30.79 KB (0%) |
| @sentry/react | 24.5 KB (+0.11% 🔺) |
| @sentry/react (incl. Tracing) | 35.81 KB (+0.08% 🔺) |
| @sentry/vue | 25.73 KB (0%) |
| @sentry/vue (incl. Tracing) | 34.59 KB (0%) |
| @sentry/svelte | 21.92 KB (0%) |
| CDN Bundle | 23.01 KB (0%) |
| CDN Bundle (incl. Tracing) | 34.27 KB (+0.01% 🔺) |
| CDN Bundle (incl. Tracing, Replay) | 68.08 KB (0%) |
| CDN Bundle (incl. Tracing, Replay, Feedback) | 73.09 KB (0%) |
| CDN Bundle - uncompressed | 67.88 KB (0%) |
| CDN Bundle (incl. Tracing) - uncompressed | 101.68 KB (0%) |
| CDN Bundle (incl. Tracing, Replay) - uncompressed | 211.58 KB (0%) |
| CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed | 223.96 KB (0%) |
| @sentry/nextjs (client) | 35.14 KB (0%) |
| @sentry/sveltekit (client) | 33.39 KB (0%) |
| @sentry/node | 114.64 KB (-0.01% 🔽) |
| @sentry/aws-serverless | 103.32 KB (+0.01% 🔺) |