Skip to content

Commit 871ca31

Browse files
authored
[app] Send errors not handled by explicit error boundaries through reportError (#76101)
1 parent 0dc87f7 commit 871ca31

File tree

8 files changed

+273
-209
lines changed

8 files changed

+273
-209
lines changed

packages/next/src/client/app-index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -244,11 +244,11 @@ function Root({ children }: React.PropsWithChildren<{}>) {
244244
return children
245245
}
246246

247-
const reactRootOptions = {
247+
const reactRootOptions: ReactDOMClient.RootOptions = {
248248
onRecoverableError,
249249
onCaughtError,
250250
onUncaughtError,
251-
} satisfies ReactDOMClient.RootOptions
251+
}
252252

253253
export function hydrate() {
254254
const reactEl = (

packages/next/src/client/components/globals/intercept-console-error.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { isNextRouterError } from '../is-next-router-error'
33
import { handleClientError } from '../errors/use-error-handler'
44
import { parseConsoleArgs } from '../../lib/console'
55

6-
export const originConsoleError = window.console.error
6+
export const originConsoleError = globalThis.console.error
77

88
// Patch console.error to collect information about hydration errors
99
export function patchConsoleError() {

packages/next/src/client/react-client-callbacks/error-boundary-callbacks.ts

Lines changed: 27 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,41 @@
11
// This file is only used in app router due to the specific error state handling.
22

3-
import type { HydrationOptions } from 'react-dom/client'
3+
import type { ErrorInfo } from 'react'
44
import { getReactStitchedError } from '../components/errors/stitched-error'
55
import { handleClientError } from '../components/errors/use-error-handler'
66
import { isNextRouterError } from '../components/is-next-router-error'
77
import { isBailoutToCSRError } from '../../shared/lib/lazy-dynamic/bailout-to-csr'
88
import { reportGlobalError } from './report-global-error'
99
import { originConsoleError } from '../components/globals/intercept-console-error'
10+
import { AppDevOverlayErrorBoundary } from '../components/react-dev-overlay/app/app-dev-overlay-error-boundary'
11+
import {
12+
ErrorBoundaryHandler,
13+
GlobalError as DefaultErrorBoundary,
14+
} from '../components/error-boundary'
15+
16+
export function onCaughtError(
17+
err: unknown,
18+
errorInfo: ErrorInfo & { errorBoundary?: React.Component }
19+
) {
20+
const errorBoundaryComponent = errorInfo.errorBoundary?.constructor
21+
22+
const isImplicitErrorBoundary =
23+
(process.env.NODE_ENV !== 'production' &&
24+
errorBoundaryComponent === AppDevOverlayErrorBoundary) ||
25+
(errorBoundaryComponent === ErrorBoundaryHandler &&
26+
(errorInfo.errorBoundary! as InstanceType<typeof ErrorBoundaryHandler>)
27+
.props.errorComponent === DefaultErrorBoundary)
28+
if (isImplicitErrorBoundary) {
29+
// We don't consider errors caught unless they're caught by an explicit error
30+
// boundary. The built-in ones are considered implicit.
31+
// This mimics how the same app would behave without Next.js.
32+
return onUncaughtError(err, errorInfo)
33+
}
1034

11-
export const onCaughtError: HydrationOptions['onCaughtError'] = (
12-
err,
13-
errorInfo
14-
) => {
1535
// Skip certain custom errors which are not expected to be reported on client
1636
if (isBailoutToCSRError(err) || isNextRouterError(err)) return
1737

1838
if (process.env.NODE_ENV !== 'production') {
19-
const errorBoundaryComponent = errorInfo?.errorBoundary?.constructor
2039
const errorBoundaryName =
2140
// read react component displayName
2241
(errorBoundaryComponent as any)?.displayName ||
@@ -57,35 +76,19 @@ export const onCaughtError: HydrationOptions['onCaughtError'] = (
5776
}
5877
}
5978

60-
export const onUncaughtError: HydrationOptions['onUncaughtError'] = (
61-
err,
62-
errorInfo
63-
) => {
79+
export function onUncaughtError(err: unknown, errorInfo: React.ErrorInfo) {
6480
// Skip certain custom errors which are not expected to be reported on client
6581
if (isBailoutToCSRError(err) || isNextRouterError(err)) return
6682

6783
if (process.env.NODE_ENV !== 'production') {
68-
const componentThatErroredFrame = errorInfo?.componentStack?.split('\n')[1]
69-
70-
// Match chrome or safari stack trace
71-
const matches =
72-
componentThatErroredFrame?.match(/\s+at (\w+)\s+|(\w+)@/) ?? []
73-
const componentThatErroredName = matches[1] || matches[2] || 'Unknown'
74-
75-
// Create error location with errored component and error boundary, to match the behavior of default React onCaughtError handler.
76-
const errorLocation = componentThatErroredName
77-
? `The above error occurred in the <${componentThatErroredName}> component.`
78-
: `The above error occurred in one of your components.`
79-
8084
const stitchedError = getReactStitchedError(err)
8185
// TODO: change to passing down errorInfo later
8286
// In development mode, pass along the component stack to the error
8387
if (errorInfo.componentStack) {
8488
;(stitchedError as any)._componentStack = errorInfo.componentStack
8589
}
8690

87-
// Log and report the error with location but without modifying the error stack
88-
originConsoleError('%o\n\n%s', err, errorLocation)
91+
// TODO: Add an adendum to the overlay telling people about custom error boundaries.
8992
reportGlobalError(stitchedError)
9093
} else {
9194
reportGlobalError(err)

packages/next/src/client/react-client-callbacks/report-global-error.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ export const reportGlobalError =
44
// emulating an uncaught JavaScript error.
55
reportError
66
: (error: unknown) => {
7-
window.console.error(error)
7+
// TODO: Dispatch error event
8+
globalThis.console.error(error)
89
}

0 commit comments

Comments
 (0)