diff --git a/packages/next/src/client/components/errors/stitched-error.ts b/packages/next/src/client/components/errors/stitched-error.ts
index e4da4fe4869e2..45cfe96096a83 100644
--- a/packages/next/src/client/components/errors/stitched-error.ts
+++ b/packages/next/src/client/components/errors/stitched-error.ts
@@ -34,6 +34,9 @@ export function getReactStitchedError(err: T): Error | T {
}
function appendOwnerStack(error: Error) {
+ if (!React.captureOwnerStack) {
+ return
+ }
let stack = error.stack || ''
// This module is only bundled in development mode so this is safe.
const ownerStack = React.captureOwnerStack()
diff --git a/packages/next/src/client/components/errors/use-error-handler.ts b/packages/next/src/client/components/errors/use-error-handler.ts
index 4b681fb347551..6a40f1b7135c4 100644
--- a/packages/next/src/client/components/errors/use-error-handler.ts
+++ b/packages/next/src/client/components/errors/use-error-handler.ts
@@ -70,8 +70,8 @@ export function useErrorHandler(
)
// Reset error queues.
- errorQueue.splice(0, 0)
- rejectionQueue.splice(0, 0)
+ errorQueue.splice(0, errorQueue.length)
+ rejectionQueue.splice(0, rejectionQueue.length)
}
}, [handleOnUnhandledError, handleOnUnhandledRejection])
}
diff --git a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/container/runtime-error/use-error-hook.ts b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/container/runtime-error/use-error-hook.ts
index 3b0fb084bfb18..7b05b69064a50 100644
--- a/packages/next/src/client/components/react-dev-overlay/_experimental/internal/container/runtime-error/use-error-hook.ts
+++ b/packages/next/src/client/components/react-dev-overlay/_experimental/internal/container/runtime-error/use-error-hook.ts
@@ -26,12 +26,12 @@ function getErrorSignature(ev: SupportedErrorEvent): string {
case ACTION_UNHANDLED_REJECTION: {
return `${event.reason.name}::${event.reason.message}::${event.reason.stack}`
}
- default: {
- }
+ default:
+ break
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
- const _: never = event as never
+ const _ = event satisfies never
return ''
}
diff --git a/packages/next/src/client/components/react-dev-overlay/internal/container/Errors.tsx b/packages/next/src/client/components/react-dev-overlay/internal/container/Errors.tsx
index c46ac62bc2198..c62dacdd91406 100644
--- a/packages/next/src/client/components/react-dev-overlay/internal/container/Errors.tsx
+++ b/packages/next/src/client/components/react-dev-overlay/internal/container/Errors.tsx
@@ -236,6 +236,7 @@ export function Errors({
return (
@@ -256,7 +257,8 @@ export function Errors({
- {readyErrors.length} issue{readyErrors.length > 1 ? 's' : ''}
+ {readyErrors.length} issue
+ {readyErrors.length > 1 ? 's' : ''}
+ )
+}
diff --git a/test/development/app-dir/hydration-error-count/app/html-diff/page.tsx b/test/development/app-dir/hydration-error-count/app/html-diff/page.tsx
new file mode 100644
index 0000000000000..57acfd0c3aacf
--- /dev/null
+++ b/test/development/app-dir/hydration-error-count/app/html-diff/page.tsx
@@ -0,0 +1,5 @@
+'use client'
+
+export default function Page() {
+ return {typeof window === 'undefined' ? 'server' : 'client'}
+}
diff --git a/test/development/app-dir/hydration-error-count/app/layout.tsx b/test/development/app-dir/hydration-error-count/app/layout.tsx
new file mode 100644
index 0000000000000..888614deda3ba
--- /dev/null
+++ b/test/development/app-dir/hydration-error-count/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/hydration-error-count/hydration-error-count.test.ts b/test/development/app-dir/hydration-error-count/hydration-error-count.test.ts
new file mode 100644
index 0000000000000..54442ffe8a340
--- /dev/null
+++ b/test/development/app-dir/hydration-error-count/hydration-error-count.test.ts
@@ -0,0 +1,33 @@
+import { nextTestSetup } from 'e2e-utils'
+import { hasErrorToast, getToastErrorCount, retry } from 'next-test-utils'
+
+describe('hydration-error-count', () => {
+ const { next } = nextTestSetup({
+ files: __dirname,
+ })
+
+ // These error count should be consistent between normal mode and when reactOwnerStack is enabled (PPR testing)
+ it('should have correct hydration error count for bad nesting', async () => {
+ const browser = await next.browser('/bad-nesting')
+
+ await retry(async () => {
+ await hasErrorToast(browser)
+ const totalErrorCount = await getToastErrorCount(browser)
+
+ // One hydration error and one warning
+ expect(totalErrorCount).toBe(2)
+ })
+ })
+
+ it('should have correct hydration error count for html diff', async () => {
+ const browser = await next.browser('/html-diff')
+
+ await retry(async () => {
+ await hasErrorToast(browser)
+ const totalErrorCount = await getToastErrorCount(browser)
+
+ // One hydration error and one warning
+ expect(totalErrorCount).toBe(1)
+ })
+ })
+})
diff --git a/test/development/app-dir/hydration-error-count/next.config.js b/test/development/app-dir/hydration-error-count/next.config.js
new file mode 100644
index 0000000000000..807126e4cf0bf
--- /dev/null
+++ b/test/development/app-dir/hydration-error-count/next.config.js
@@ -0,0 +1,6 @@
+/**
+ * @type {import('next').NextConfig}
+ */
+const nextConfig = {}
+
+module.exports = nextConfig
diff --git a/test/lib/next-test-utils.ts b/test/lib/next-test-utils.ts
index e2a660a6ab3f7..365372b8aea8e 100644
--- a/test/lib/next-test-utils.ts
+++ b/test/lib/next-test-utils.ts
@@ -887,17 +887,32 @@ export async function assertNoRedbox(browser: BrowserInterface) {
export async function hasErrorToast(
browser: BrowserInterface
): Promise {
- return (
+ return Boolean(
+ await browser.eval(() => {
+ const portal = [].slice
+ .call(document.querySelectorAll('nextjs-portal'))
+ .find((p) => p.shadowRoot.querySelector('[data-issues]'))
+
+ const root = portal?.shadowRoot
+ const node = root?.querySelector('[data-issues-count]')
+ return !!node
+ })
+ )
+}
+
+export async function getToastErrorCount(
+ browser: BrowserInterface
+): Promise {
+ return parseInt(
(await browser.eval(() => {
- return Boolean(
- [].slice.call(document.querySelectorAll('nextjs-portal')).find((p) =>
- p.shadowRoot.querySelector(
- // TODO(jiwon): data-nextjs-toast may not be an error indicator in new UI
- isNewDevOverlay ? '[data-issues]' : '[data-nextjs-toast]'
- )
- )
- )
- })) ?? false // When browser.eval() throws, it returns null.
+ const portal = [].slice
+ .call(document.querySelectorAll('nextjs-portal'))
+ .find((p) => p.shadowRoot.querySelector('[data-issues]'))
+
+ const root = portal?.shadowRoot
+ const node = root?.querySelector('[data-issues-count]')
+ return node?.innerText || '0'
+ })) ?? 0
)
}