Skip to content

feat(react)!: Update ErrorBoundary componentStack type #14742

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jan 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott

Work in this release was contributed by @nwalters512, @aloisklink, @arturovt, @benjick, @maximepvrt, and @mstrokin. Thank you for your contributions!
Work in this release was contributed by @nwalters512, @aloisklink, @arturovt, @benjick, @maximepvrt, @mstrokin, and @kunal-511. Thank you for your contributions!

- **feat(solidstart)!: Default to `--import` setup and add `autoInjectServerSentry` ([#14862](https://github.com/getsentry/sentry-javascript/pull/14862))**

Expand Down
6 changes: 6 additions & 0 deletions docs/migration/v8-to-v9.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,12 @@ Older Typescript versions _may_ still work, but we will not test them anymore an

To customize which files are deleted after upload, define the `filesToDeleteAfterUpload` array with globs.

### `@sentry/react`

The `componentStack` field in the `ErrorBoundary` component is now typed as `string` instead of `string | null | undefined` for the `onError` and `onReset` lifecycle methods. This more closely matches the actual behavior of React, which always returns a `string` whenever a component stack is available.

In the `onUnmount` lifecycle method, the `componentStack` field is now typed as `string | null`. The `componentStack` is `null` when no error has been thrown at time of unmount.

### Uncategorized (TODO)

TODO
Expand Down
97 changes: 58 additions & 39 deletions packages/react/src/errorboundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ export type FallbackRender = (errorData: {
resetError(): void;
}) => React.ReactElement;

type OnUnmountType = {
(error: null, componentStack: null, eventId: null): void;
(error: unknown, componentStack: string, eventId: string): void;
};

export type ErrorBoundaryProps = {
children?: React.ReactNode | (() => React.ReactNode);
/** If a Sentry report dialog should be rendered on error */
Expand All @@ -42,15 +47,23 @@ export type ErrorBoundaryProps = {
*/
handled?: boolean | undefined;
/** Called when the error boundary encounters an error */
onError?: ((error: unknown, componentStack: string | undefined, eventId: string) => void) | undefined;
onError?: ((error: unknown, componentStack: string, eventId: string) => void) | undefined;
/** Called on componentDidMount() */
onMount?: (() => void) | undefined;
/** Called if resetError() is called from the fallback render props function */
onReset?: ((error: unknown, componentStack: string | null | undefined, eventId: string | null) => void) | undefined;
/** Called on componentWillUnmount() */
onUnmount?: ((error: unknown, componentStack: string | null | undefined, eventId: string | null) => void) | undefined;
/**
* Called when the error boundary resets due to a reset call from the
* fallback render props function.
*/
onReset?: ((error: unknown, componentStack: string, eventId: string) => void) | undefined;
/**
* Called on componentWillUnmount() with the error, componentStack, and eventId.
*
* If the error boundary never encountered an error, the error
* componentStack, and eventId will be null.
*/
onUnmount?: OnUnmountType | undefined;
/** Called before the error is captured by Sentry, allows for you to add tags or context using the scope */
beforeCapture?: ((scope: Scope, error: unknown, componentStack: string | undefined) => void) | undefined;
beforeCapture?: ((scope: Scope, error: unknown, componentStack: string) => void) | undefined;
};

type ErrorBoundaryState =
Expand All @@ -65,7 +78,7 @@ type ErrorBoundaryState =
eventId: string;
};

const INITIAL_STATE = {
const INITIAL_STATE: ErrorBoundaryState = {
componentStack: null,
error: null,
eventId: null,
Expand Down Expand Up @@ -104,20 +117,17 @@ class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundarySta

public componentDidCatch(error: unknown, errorInfo: React.ErrorInfo): void {
const { componentStack } = errorInfo;
// TODO(v9): Remove this check and type `componentStack` to be React.ErrorInfo['componentStack'].
const passedInComponentStack: string | undefined = componentStack == null ? undefined : componentStack;

const { beforeCapture, onError, showDialog, dialogOptions } = this.props;
withScope(scope => {
if (beforeCapture) {
beforeCapture(scope, error, passedInComponentStack);
beforeCapture(scope, error, componentStack);
}

const handled = this.props.handled != null ? this.props.handled : !!this.props.fallback;
const eventId = captureReactException(error, errorInfo, { mechanism: { handled } });

if (onError) {
onError(error, passedInComponentStack, eventId);
onError(error, componentStack, eventId);
}
if (showDialog) {
this._lastEventId = eventId;
Expand All @@ -143,7 +153,15 @@ class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundarySta
const { error, componentStack, eventId } = this.state;
const { onUnmount } = this.props;
if (onUnmount) {
onUnmount(error, componentStack, eventId);
if (this.state === INITIAL_STATE) {
// If the error boundary never encountered an error, call onUnmount with null values
onUnmount(null, null, null);
} else {
// `componentStack` and `eventId` are guaranteed to be non-null here because `onUnmount` is only called
// when the error boundary has already encountered an error.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
onUnmount(error, componentStack!, eventId!);
}
}

if (this._cleanupHook) {
Expand All @@ -156,7 +174,10 @@ class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundarySta
const { onReset } = this.props;
const { error, componentStack, eventId } = this.state;
if (onReset) {
onReset(error, componentStack, eventId);
// `componentStack` and `eventId` are guaranteed to be non-null here because `onReset` is only called
// when the error boundary has already encountered an error.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
onReset(error, componentStack!, eventId!);
}
this.setState(INITIAL_STATE);
}
Expand All @@ -165,35 +186,33 @@ class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundarySta
const { fallback, children } = this.props;
const state = this.state;

if (state.error) {
let element: React.ReactElement | undefined = undefined;
if (typeof fallback === 'function') {
element = React.createElement(fallback, {
error: state.error,
componentStack: state.componentStack as string,
resetError: this.resetErrorBoundary.bind(this),
eventId: state.eventId as string,
});
} else {
element = fallback;
}

if (React.isValidElement(element)) {
return element;
}

if (fallback) {
DEBUG_BUILD && logger.warn('fallback did not produce a valid ReactElement');
}
// `componentStack` is only null in the initial state, when no error has been captured.
// If an error has been captured, `componentStack` will be a string.
// We cannot check `state.error` because null can be thrown as an error.
if (state.componentStack === null) {
return typeof children === 'function' ? children() : children;
}

// Fail gracefully if no fallback provided or is not valid
return null;
const element =
typeof fallback === 'function'
? React.createElement(fallback, {
error: state.error,
componentStack: state.componentStack,
resetError: () => this.resetErrorBoundary(),
eventId: state.eventId,
})
: fallback;

if (React.isValidElement(element)) {
return element;
}

if (typeof children === 'function') {
return (children as () => React.ReactNode)();
if (fallback) {
DEBUG_BUILD && logger.warn('fallback did not produce a valid ReactElement');
}
return children;

// Fail gracefully if no fallback provided or is not valid
return null;
}
}

Expand Down
Loading