diff --git a/packages/next/src/client/components/react-dev-overlay/app/ReactDevOverlay.tsx b/packages/next/src/client/components/react-dev-overlay/app/ReactDevOverlay.tsx
index 75cfeb06eb94b..2b46347d4fa2d 100644
--- a/packages/next/src/client/components/react-dev-overlay/app/ReactDevOverlay.tsx
+++ b/packages/next/src/client/components/react-dev-overlay/app/ReactDevOverlay.tsx
@@ -1,9 +1,8 @@
import React from 'react'
-import { ACTION_UNHANDLED_ERROR, type OverlayState } from '../shared'
+import type { OverlayState } from '../shared'
import { ShadowPortal } from '../internal/components/ShadowPortal'
import { BuildError } from '../internal/container/BuildError'
-import { Errors, type SupportedErrorEvent } from '../internal/container/Errors'
-import { parseStack } from '../internal/helpers/parse-stack'
+import { Errors } from '../internal/container/Errors'
import { StaticIndicator } from '../internal/container/StaticIndicator'
import { Base } from '../internal/styles/Base'
import { ComponentStyles } from '../internal/styles/ComponentStyles'
@@ -13,7 +12,7 @@ import type { Dispatcher } from './hot-reloader-client'
import { RuntimeErrorHandler } from '../internal/helpers/runtime-error-handler'
interface ReactDevOverlayState {
- reactError: SupportedErrorEvent | null
+ isReactError: boolean
}
export default class ReactDevOverlay extends React.PureComponent<
{
@@ -23,27 +22,20 @@ export default class ReactDevOverlay extends React.PureComponent<
},
ReactDevOverlayState
> {
- state = { reactError: null }
+ state = { isReactError: false }
static getDerivedStateFromError(error: Error): ReactDevOverlayState {
- if (!error.stack) return { reactError: null }
+ if (!error.stack) return { isReactError: false }
RuntimeErrorHandler.hadRuntimeError = true
return {
- reactError: {
- id: 0,
- event: {
- type: ACTION_UNHANDLED_ERROR,
- reason: error,
- frames: parseStack(error.stack),
- },
- },
+ isReactError: true,
}
}
render() {
const { state, children, dispatcher } = this.props
- const { reactError } = this.state
+ const { isReactError } = this.state
const hasBuildError = state.buildError != null
const hasRuntimeErrors = Boolean(state.errors.length)
@@ -52,7 +44,7 @@ export default class ReactDevOverlay extends React.PureComponent<
return (
<>
- {reactError ? (
+ {isReactError ? (
@@ -78,8 +70,10 @@ export default class ReactDevOverlay extends React.PureComponent<
{hasRuntimeErrors ? (
{
- const stitchedError = getReactStitchedError(err)
+ // x-ref: https://github.com/facebook/react/pull/28736
+ const cause = isError(error) && 'cause' in error ? error.cause : error
+ const stitchedError = getReactStitchedError(cause)
// In development mode, pass along the component stack to the error
if (process.env.NODE_ENV === 'development' && errorInfo.componentStack) {
;(stitchedError as any)._componentStack = errorInfo.componentStack
}
// Skip certain custom errors which are not expected to be reported on client
- if (isBailoutToCSRError(err)) return
+ if (isBailoutToCSRError(cause)) return
reportGlobalError(stitchedError)
}
diff --git a/test/development/acceptance-app/dynamic-error.test.ts b/test/development/acceptance-app/dynamic-error.test.ts
index 1e7e53583598f..258945892e6b4 100644
--- a/test/development/acceptance-app/dynamic-error.test.ts
+++ b/test/development/acceptance-app/dynamic-error.test.ts
@@ -7,31 +7,30 @@ import { outdent } from 'outdent'
describe('dynamic = "error" in devmode', () => {
const { next } = nextTestSetup({
files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')),
- skipStart: true,
})
it('should show error overlay when dynamic is forced', async () => {
- const { session, cleanup } = await sandbox(next, undefined, '/server')
-
- // dynamic = "error" and force dynamic
- await session.patch(
- 'app/server/page.js',
- outdent`
- import { cookies } from 'next/headers';
-
- import Component from '../../index'
-
- export default async function Page() {
- await cookies()
- return
- }
-
- export const dynamic = "error"
- `
+ const { session, cleanup } = await sandbox(
+ next,
+ new Map([
+ [
+ 'app/server/page.js',
+ outdent`
+ import { cookies } from 'next/headers';
+
+ export default async function Page() {
+ await cookies()
+ return null
+ }
+
+ export const dynamic = "error"
+ `,
+ ],
+ ]),
+ '/server'
)
await session.assertHasRedbox()
- console.log(await session.getRedboxDescription())
expect(await session.getRedboxDescription()).toMatchInlineSnapshot(
`"[ Server ] Error: Route /server with \`dynamic = "error"\` couldn't be rendered statically because it used \`cookies\`. See more info here: https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering"`
)
diff --git a/test/development/app-dir/owner-stack-invalid-element-type/app/browser/browser-only.js b/test/development/app-dir/owner-stack-invalid-element-type/app/browser/browser-only.js
new file mode 100644
index 0000000000000..dc00bde91399b
--- /dev/null
+++ b/test/development/app-dir/owner-stack-invalid-element-type/app/browser/browser-only.js
@@ -0,0 +1,11 @@
+'use client'
+
+import Foo from '../foo'
+
+export default function BrowserOnly() {
+ return (
+
+
+
+ )
+}
diff --git a/test/development/app-dir/owner-stack-invalid-element-type/app/browser/page.js b/test/development/app-dir/owner-stack-invalid-element-type/app/browser/page.js
new file mode 100644
index 0000000000000..77875a1db1096
--- /dev/null
+++ b/test/development/app-dir/owner-stack-invalid-element-type/app/browser/page.js
@@ -0,0 +1,11 @@
+'use client'
+
+import dynamic from 'next/dynamic'
+
+const BrowserOnly = dynamic(() => import('./browser-only'), {
+ ssr: false,
+})
+
+export default function Page() {
+ return
+}
diff --git a/test/development/app-dir/owner-stack-invalid-element-type/app/foo.js b/test/development/app-dir/owner-stack-invalid-element-type/app/foo.js
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/test/development/app-dir/owner-stack-invalid-element-type/app/layout.tsx b/test/development/app-dir/owner-stack-invalid-element-type/app/layout.tsx
new file mode 100644
index 0000000000000..888614deda3ba
--- /dev/null
+++ b/test/development/app-dir/owner-stack-invalid-element-type/app/layout.tsx
@@ -0,0 +1,8 @@
+import { ReactNode } from 'react'
+export default function Root({ children }: { children: ReactNode }) {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/test/development/app-dir/owner-stack-invalid-element-type/app/rsc/page.js b/test/development/app-dir/owner-stack-invalid-element-type/app/rsc/page.js
new file mode 100644
index 0000000000000..3b835737b3983
--- /dev/null
+++ b/test/development/app-dir/owner-stack-invalid-element-type/app/rsc/page.js
@@ -0,0 +1,9 @@
+import Foo from '../foo'
+
+export default function Page() {
+ return (
+
+
+
+ )
+}
diff --git a/test/development/app-dir/owner-stack-invalid-element-type/app/ssr/page.js b/test/development/app-dir/owner-stack-invalid-element-type/app/ssr/page.js
new file mode 100644
index 0000000000000..297ed0e3ef260
--- /dev/null
+++ b/test/development/app-dir/owner-stack-invalid-element-type/app/ssr/page.js
@@ -0,0 +1,11 @@
+'use client'
+
+import Foo from '../foo'
+
+export default function Page() {
+ return (
+
+
+
+ )
+}
diff --git a/test/development/app-dir/owner-stack-invalid-element-type/next.config.js b/test/development/app-dir/owner-stack-invalid-element-type/next.config.js
new file mode 100644
index 0000000000000..d14b1bf8fdc37
--- /dev/null
+++ b/test/development/app-dir/owner-stack-invalid-element-type/next.config.js
@@ -0,0 +1,10 @@
+/**
+ * @type {import('next').NextConfig}
+ */
+const nextConfig = {
+ experimental: {
+ reactOwnerStack: true,
+ },
+}
+
+module.exports = nextConfig
diff --git a/test/development/app-dir/owner-stack-invalid-element-type/owner-stack-invalid-element-type.test.ts b/test/development/app-dir/owner-stack-invalid-element-type/owner-stack-invalid-element-type.test.ts
new file mode 100644
index 0000000000000..0e980f3a2239b
--- /dev/null
+++ b/test/development/app-dir/owner-stack-invalid-element-type/owner-stack-invalid-element-type.test.ts
@@ -0,0 +1,147 @@
+import { nextTestSetup } from 'e2e-utils'
+import { assertHasRedbox, getRedboxSource } from 'next-test-utils'
+
+async function getStackFramesContent(browser) {
+ const stackFrameElements = await browser.elementsByCss(
+ '[data-nextjs-call-stack-frame]'
+ )
+ const stackFramesContent = (
+ await Promise.all(
+ stackFrameElements.map(async (frame) => {
+ const functionNameEl = await frame.$('[data-nextjs-frame-expanded]')
+ const sourceEl = await frame.$('[data-has-source]')
+ const functionName = functionNameEl
+ ? await functionNameEl.innerText()
+ : ''
+ const source = sourceEl ? await sourceEl.innerText() : ''
+
+ if (!functionName) {
+ return ''
+ }
+ return `at ${functionName} (${source})`
+ })
+ )
+ )
+ .filter(Boolean)
+ .join('\n')
+
+ return stackFramesContent
+}
+
+describe('app-dir - owner-stack-invalid-element-type', () => {
+ const { next } = nextTestSetup({
+ files: __dirname,
+ })
+
+ it('should catch invalid element from a client-only component', async () => {
+ const browser = await next.browser('/browser')
+
+ await assertHasRedbox(browser)
+ const source = await getRedboxSource(browser)
+
+ const stackFramesContent = await getStackFramesContent(browser)
+ if (process.env.TURBOPACK) {
+ expect(stackFramesContent).toMatchInlineSnapshot(
+ `"at Page (app/browser/page.js (10:10))"`
+ )
+ expect(source).toMatchInlineSnapshot(`
+ "app/browser/browser-only.js (8:7) @ BrowserOnly
+
+ 6 | return (
+ 7 |
+ > 8 |
+ | ^
+ 9 |
+ 10 | )
+ 11 | }"
+ `)
+ } else {
+ expect(stackFramesContent).toMatchInlineSnapshot(
+ `"at BrowserOnly (app/browser/page.js (10:11))"`
+ )
+ expect(source).toMatchInlineSnapshot(`
+ "app/browser/browser-only.js (8:8) @ Foo
+
+ 6 | return (
+ 7 |
+ > 8 |
+ | ^
+ 9 |
+ 10 | )
+ 11 | }"
+ `)
+ }
+ })
+
+ it('should catch invalid element from a rsc component', async () => {
+ const browser = await next.browser('/rsc')
+
+ await assertHasRedbox(browser)
+ const stackFramesContent = await getStackFramesContent(browser)
+ const source = await getRedboxSource(browser)
+
+ if (process.env.TURBOPACK) {
+ expect(stackFramesContent).toMatchInlineSnapshot(`""`)
+ expect(source).toMatchInlineSnapshot(`
+ "app/rsc/page.js (6:7) @ Page
+
+ 4 | return (
+ 5 |
+ > 6 |
+ | ^
+ 7 |
+ 8 | )
+ 9 | }"
+ `)
+ } else {
+ expect(stackFramesContent).toMatchInlineSnapshot(`""`)
+ expect(source).toMatchInlineSnapshot(`
+ "app/rsc/page.js (6:8) @ Foo
+
+ 4 | return (
+ 5 |
+ > 6 |
+ | ^
+ 7 |
+ 8 | )
+ 9 | }"
+ `)
+ }
+ })
+
+ it('should catch invalid element from on ssr client component', async () => {
+ const browser = await next.browser('/ssr')
+
+ await assertHasRedbox(browser)
+
+ const stackFramesContent = await getStackFramesContent(browser)
+ const source = await getRedboxSource(browser)
+ if (process.env.TURBOPACK) {
+ expect(stackFramesContent).toMatchInlineSnapshot(`""`)
+ expect(source).toMatchInlineSnapshot(`
+ "app/ssr/page.js (8:7) @ Page
+
+ 6 | return (
+ 7 |
+ > 8 |
+ | ^
+ 9 |
+ 10 | )
+ 11 | }"
+ `)
+ } else {
+ expect(stackFramesContent).toMatchInlineSnapshot(`""`)
+ expect(source).toMatchInlineSnapshot(`
+ "app/ssr/page.js (8:8) @ Foo
+
+ 6 | return (
+ 7 |
+ > 8 |
+ | ^
+ 9 |
+ 10 | )
+ 11 | }"
+ `)
+ }
+ })
+})