Skip to content
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
4 changes: 2 additions & 2 deletions packages/next/src/client/app-index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -244,11 +244,11 @@ function Root({ children }: React.PropsWithChildren<{}>) {
return children
}

const reactRootOptions = {
const reactRootOptions: ReactDOMClient.RootOptions = {
onRecoverableError,
onCaughtError,
onUncaughtError,
} satisfies ReactDOMClient.RootOptions
}

export function hydrate() {
const reactEl = (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { isNextRouterError } from '../is-next-router-error'
import { handleClientError } from '../errors/use-error-handler'
import { parseConsoleArgs } from '../../lib/console'

export const originConsoleError = window.console.error
export const originConsoleError = globalThis.console.error

// Patch console.error to collect information about hydration errors
export function patchConsoleError() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,41 @@
// This file is only used in app router due to the specific error state handling.

import type { HydrationOptions } from 'react-dom/client'
import type { ErrorInfo } from 'react'
import { getReactStitchedError } from '../components/errors/stitched-error'
import { handleClientError } from '../components/errors/use-error-handler'
import { isNextRouterError } from '../components/is-next-router-error'
import { isBailoutToCSRError } from '../../shared/lib/lazy-dynamic/bailout-to-csr'
import { reportGlobalError } from './report-global-error'
import { originConsoleError } from '../components/globals/intercept-console-error'
import { AppDevOverlayErrorBoundary } from '../components/react-dev-overlay/app/app-dev-overlay-error-boundary'
import {
ErrorBoundaryHandler,
GlobalError as DefaultErrorBoundary,
} from '../components/error-boundary'

export function onCaughtError(
err: unknown,
errorInfo: ErrorInfo & { errorBoundary?: React.Component }
) {
const errorBoundaryComponent = errorInfo.errorBoundary?.constructor

const isImplicitErrorBoundary =
(process.env.NODE_ENV !== 'production' &&
errorBoundaryComponent === AppDevOverlayErrorBoundary) ||
(errorBoundaryComponent === ErrorBoundaryHandler &&
Copy link
Member

Choose a reason for hiding this comment

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

This logic seem easy to break, can we check if the react error lifecyle methods are present then we treat it as an implicit error boundary? constructor.getDerivedStateFromError || errorBoundary.componentDidCatch

Copy link
Member Author

Choose a reason for hiding this comment

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

That would also match explicit error boundaries.

When would this break?

Copy link
Member Author

Choose a reason for hiding this comment

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

It would actually always be true. errorInfo.errorBoundary will always be an error boundary so either cDC or gDSFE must be defined.

(errorInfo.errorBoundary! as InstanceType<typeof ErrorBoundaryHandler>)
.props.errorComponent === DefaultErrorBoundary)
if (isImplicitErrorBoundary) {
// We don't consider errors caught unless they're caught by an explicit error
// boundary. The built-in ones are considered implicit.
// This mimics how the same app would behave without Next.js.
return onUncaughtError(err, errorInfo)
}

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

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

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

if (process.env.NODE_ENV !== 'production') {
const componentThatErroredFrame = errorInfo?.componentStack?.split('\n')[1]

// Match chrome or safari stack trace
const matches =
componentThatErroredFrame?.match(/\s+at (\w+)\s+|(\w+)@/) ?? []
const componentThatErroredName = matches[1] || matches[2] || 'Unknown'

// Create error location with errored component and error boundary, to match the behavior of default React onCaughtError handler.
const errorLocation = componentThatErroredName
? `The above error occurred in the <${componentThatErroredName}> component.`
: `The above error occurred in one of your components.`

const stitchedError = getReactStitchedError(err)
// TODO: change to passing down errorInfo later
// In development mode, pass along the component stack to the error
if (errorInfo.componentStack) {
;(stitchedError as any)._componentStack = errorInfo.componentStack
}

// Log and report the error with location but without modifying the error stack
originConsoleError('%o\n\n%s', err, errorLocation)
// TODO: Add an adendum to the overlay telling people about custom error boundaries.
reportGlobalError(stitchedError)
} else {
reportGlobalError(err)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ export const reportGlobalError =
// emulating an uncaught JavaScript error.
reportError
: (error: unknown) => {
window.console.error(error)
// TODO: Dispatch error event
globalThis.console.error(error)
}
Loading
Loading