Skip to content

Commit d6f9e08

Browse files
authored
[dev-overlay] Stop appending wrong Owner Stacks to SSR-only shell errors (#77302)
1 parent 166251d commit d6f9e08

File tree

6 files changed

+53
-54
lines changed

6 files changed

+53
-54
lines changed

packages/next/src/client/components/errors/stitched-error.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,13 @@ export function setOwnerStack(error: Error, stack: string | null) {
1818
ownerStacks.set(error, stack)
1919
}
2020

21-
export function getReactStitchedError(err: unknown): Error {
22-
const newError = isError(err)
23-
? err
24-
: // TODO: stringify thrown value
25-
new Error('Thrown value was ignored. This is a bug in Next.js.')
21+
export function coerceError(value: unknown): Error {
22+
return isError(value) ? value : new Error('' + value)
23+
}
2624

25+
export function setOwnerStackIfAvailable(error: Error): void {
2726
// React 18 and prod does not have `captureOwnerStack`
2827
if ('captureOwnerStack' in React) {
29-
// TODO: Hoist these to callsites to ensure we set the correct Owner Stack.
30-
setOwnerStack(newError, React.captureOwnerStack())
28+
setOwnerStack(error, React.captureOwnerStack())
3129
}
32-
33-
return newError
3430
}

packages/next/src/client/components/errors/use-error-handler.ts

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { isNextRouterError } from '../is-next-router-error'
33
import { formatConsoleArgs, parseConsoleArgs } from '../../lib/console'
44
import isError from '../../../lib/is-error'
55
import { createConsoleError } from './console-error'
6-
import { getReactStitchedError } from '../errors/stitched-error'
6+
import { coerceError, setOwnerStackIfAvailable } from '../errors/stitched-error'
77

88
const queueMicroTask =
99
globalThis.queueMicrotask || ((cb: () => void) => Promise.resolve().then(cb))
@@ -29,7 +29,7 @@ export function handleConsoleError(
2929
environmentName
3030
)
3131
}
32-
error = getReactStitchedError(error)
32+
setOwnerStackIfAvailable(error)
3333

3434
errorQueue.push(error)
3535
for (const handler of errorHandlers) {
@@ -41,17 +41,7 @@ export function handleConsoleError(
4141
}
4242
}
4343

44-
export function handleClientError(originError: unknown) {
45-
let error: Error
46-
if (isError(originError)) {
47-
error = originError
48-
} else {
49-
// If it's not an error, format the args into an error
50-
const formattedErrorMessage = originError + ''
51-
error = new Error(formattedErrorMessage)
52-
}
53-
error = getReactStitchedError(error)
54-
44+
export function handleClientError(error: Error) {
5545
errorQueue.push(error)
5646
for (const handler of errorHandlers) {
5747
// Delayed the error being passed to React Dev Overlay,
@@ -91,28 +81,30 @@ export function useErrorHandler(
9181
}
9282

9383
function onUnhandledError(event: WindowEventMap['error']): void | boolean {
94-
if (isNextRouterError(event.error)) {
84+
const thrownValue: unknown = event.error
85+
if (isNextRouterError(thrownValue)) {
9586
event.preventDefault()
9687
return false
9788
}
9889
// When there's an error property present, we log the error to error overlay.
9990
// Otherwise we don't do anything as it's not logging in the console either.
100-
if (event.error) {
101-
handleClientError(event.error)
91+
if (thrownValue) {
92+
const error = coerceError(thrownValue)
93+
setOwnerStackIfAvailable(error)
94+
95+
handleClientError(error)
10296
}
10397
}
10498

10599
function onUnhandledRejection(ev: WindowEventMap['unhandledrejection']): void {
106-
const reason = ev?.reason
100+
const reason: unknown = ev?.reason
107101
if (isNextRouterError(reason)) {
108102
ev.preventDefault()
109103
return
110104
}
111105

112-
let error = reason
113-
if (error && !isError(error)) {
114-
error = new Error(error + '')
115-
}
106+
const error = coerceError(reason)
107+
setOwnerStackIfAvailable(error)
116108

117109
rejectionQueue.push(error)
118110
for (const handler of rejectionHandlers) {

packages/next/src/client/components/react-dev-overlay/app/app-dev-overlay.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ function ReplaySsrOnlyErrors({
5252
// eslint-disable-next-line react-hooks/rules-of-hooks
5353
useEffect(() => {
5454
if (ssrError !== null) {
55-
// TODO(veil): Produces wrong Owner Stack
55+
// TODO(veil): Include original Owner Stack (NDX-905)
5656
// TODO(veil): Mark as recoverable error
5757
// TODO(veil): console.error
5858
handleClientError(ssrError)

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

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22

33
import type { ErrorInfo } from 'react'
44
import {
5-
getReactStitchedError,
5+
setOwnerStackIfAvailable,
66
setComponentStack,
7+
coerceError,
78
} from '../components/errors/stitched-error'
89
import { handleClientError } from '../components/errors/use-error-handler'
910
import { isNextRouterError } from '../components/is-next-router-error'
@@ -16,7 +17,7 @@ import {
1617
} from '../components/error-boundary'
1718

1819
export function onCaughtError(
19-
err: unknown,
20+
thrownValue: unknown,
2021
errorInfo: ErrorInfo & { errorBoundary?: React.Component }
2122
) {
2223
const errorBoundaryComponent = errorInfo.errorBoundary?.constructor
@@ -41,11 +42,11 @@ export function onCaughtError(
4142
// We don't consider errors caught unless they're caught by an explicit error
4243
// boundary. The built-in ones are considered implicit.
4344
// This mimics how the same app would behave without Next.js.
44-
return onUncaughtError(err, errorInfo)
45+
return onUncaughtError(thrownValue, errorInfo)
4546
}
4647

4748
// Skip certain custom errors which are not expected to be reported on client
48-
if (isBailoutToCSRError(err) || isNextRouterError(err)) return
49+
if (isBailoutToCSRError(thrownValue) || isNextRouterError(thrownValue)) return
4950

5051
if (process.env.NODE_ENV !== 'production') {
5152
const errorBoundaryName =
@@ -72,37 +73,43 @@ export function onCaughtError(
7273

7374
const errorLocation = `${componentErrorMessage} ${errorBoundaryMessage}`
7475

75-
const stitchedError = getReactStitchedError(err)
76+
const error = coerceError(thrownValue)
77+
setOwnerStackIfAvailable(error)
7678
// TODO: change to passing down errorInfo later
7779
// In development mode, pass along the component stack to the error
7880
if (errorInfo.componentStack) {
79-
setComponentStack(stitchedError, errorInfo.componentStack)
81+
setComponentStack(error, errorInfo.componentStack)
8082
}
8183

8284
// Log and report the error with location but without modifying the error stack
83-
originConsoleError('%o\n\n%s', err, errorLocation)
85+
originConsoleError('%o\n\n%s', thrownValue, errorLocation)
8486

85-
handleClientError(stitchedError)
87+
handleClientError(error)
8688
} else {
87-
originConsoleError(err)
89+
originConsoleError(thrownValue)
8890
}
8991
}
9092

91-
export function onUncaughtError(err: unknown, errorInfo: React.ErrorInfo) {
93+
export function onUncaughtError(
94+
thrownValue: unknown,
95+
errorInfo: React.ErrorInfo
96+
) {
9297
// Skip certain custom errors which are not expected to be reported on client
93-
if (isBailoutToCSRError(err) || isNextRouterError(err)) return
98+
if (isBailoutToCSRError(thrownValue) || isNextRouterError(thrownValue)) return
9499

95100
if (process.env.NODE_ENV !== 'production') {
96-
const stitchedError = getReactStitchedError(err)
101+
const error = coerceError(thrownValue)
102+
setOwnerStackIfAvailable(error)
103+
97104
// TODO: change to passing down errorInfo later
98105
// In development mode, pass along the component stack to the error
99106
if (errorInfo.componentStack) {
100-
setComponentStack(stitchedError, errorInfo.componentStack)
107+
setComponentStack(error, errorInfo.componentStack)
101108
}
102109

103110
// TODO: Add an adendum to the overlay telling people about custom error boundaries.
104-
reportGlobalError(stitchedError)
111+
reportGlobalError(error)
105112
} else {
106-
reportGlobalError(err)
113+
reportGlobalError(thrownValue)
107114
}
108115
}

packages/next/src/client/react-client-callbacks/on-recoverable-error.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,29 @@
22

33
import type { HydrationOptions } from 'react-dom/client'
44
import { isBailoutToCSRError } from '../../shared/lib/lazy-dynamic/bailout-to-csr'
5-
import { reportGlobalError } from './report-global-error'
65
import {
7-
getReactStitchedError,
6+
setOwnerStackIfAvailable,
87
setComponentStack,
8+
coerceError,
99
} from '../components/errors/stitched-error'
1010
import isError from '../../lib/is-error'
11+
import { reportGlobalError } from './report-global-error'
1112

1213
export const onRecoverableError: HydrationOptions['onRecoverableError'] = (
1314
error,
1415
errorInfo
1516
) => {
1617
// x-ref: https://github.com/facebook/react/pull/28736
1718
const cause = isError(error) && 'cause' in error ? error.cause : error
18-
const stitchedError = getReactStitchedError(cause)
19-
if (process.env.NODE_ENV === 'development' && errorInfo.componentStack) {
20-
setComponentStack(stitchedError, errorInfo.componentStack)
21-
}
2219
// Skip certain custom errors which are not expected to be reported on client
2320
if (isBailoutToCSRError(cause)) return
2421

25-
reportGlobalError(stitchedError)
22+
const causeError = coerceError(cause)
23+
setOwnerStackIfAvailable(causeError)
24+
25+
if (process.env.NODE_ENV === 'development' && errorInfo.componentStack) {
26+
setComponentStack(causeError, errorInfo.componentStack)
27+
}
28+
29+
reportGlobalError(causeError)
2630
}

test/development/app-dir/ssr-only-error/ssr-only-error.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ describe('ssr-only-error', () => {
99
it('should show ssr only error in error overlay', async () => {
1010
const browser = await next.browser('/')
1111

12-
// TODO(veil): Missing Owner Stack
12+
// TODO(veil): Missing Owner Stack (NDX-905)
1313
await expect(browser).toDisplayCollapsedRedbox(`
1414
{
1515
"description": "Error: SSR only error",

0 commit comments

Comments
 (0)