diff --git a/packages/next/src/lib/metadata/metadata.tsx b/packages/next/src/lib/metadata/metadata.tsx
index 1be2eb2e20..35be61e61b 100644
--- a/packages/next/src/lib/metadata/metadata.tsx
+++ b/packages/next/src/lib/metadata/metadata.tsx
@@ -96,9 +96,10 @@ export function createMetadataComponents({
resolve(undefined)
} else {
error = resolvedError
- // If the error triggers in initial metadata resolving, re-resolve with proper error type.
- // They'll be saved for flight data, when hydrates, it will replaces the SSR'd metadata with this.
- // for not-found error: resolve not-found metadata
+ // If a not-found error is triggered during metadata resolution, we want to capture the metadata
+ // for the not-found route instead of whatever triggered the error. For all error types, we resolve an
+ // error, which will cause the outlet to throw it so it'll be handled by an error boundary
+ // (either an actual error, or an internal error that renders UI such as the NotFoundBoundary).
if (!errorType && isNotFoundError(resolvedError)) {
const [notFoundMetadataError, notFoundMetadata, notFoundViewport] =
await resolveMetadata({
diff --git a/packages/next/src/server/app-render/create-component-tree.tsx b/packages/next/src/server/app-render/create-component-tree.tsx
index 0ea1774598..37dcc63261 100644
--- a/packages/next/src/server/app-render/create-component-tree.tsx
+++ b/packages/next/src/server/app-render/create-component-tree.tsx
@@ -364,7 +364,8 @@ async function createComponentTreeInternal({
const parallelRouteMap = await Promise.all(
Object.keys(parallelRoutes).map(
async (
- parallelRouteKey
+ parallelRouteKey,
+ parallelRouteIndex
): Promise<[string, React.ReactNode, CacheNodeSeedData | null]> => {
const isChildrenRouteKey = parallelRouteKey === 'children'
const currentSegmentPath: FlightSegmentPath = firstItem
@@ -443,7 +444,11 @@ async function createComponentTreeInternal({
injectedJS: injectedJSWithCurrentLayout,
injectedFontPreloadTags: injectedFontPreloadTagsWithCurrentLayout,
asNotFound,
- metadataOutlet,
+ // The metadataOutlet is responsible for throwing any errors that were caught during metadata resolution.
+ // We only want to render an outlet once per segment, as otherwise the error will be triggered
+ // multiple times causing an uncaught error.
+ metadataOutlet:
+ parallelRouteIndex === 0 ? metadataOutlet : undefined,
ctx,
missingSlots,
})
diff --git a/test/e2e/app-dir/parallel-route-not-found/app/@bar/not-found-metadata/no-page/page.tsx b/test/e2e/app-dir/parallel-route-not-found/app/@bar/not-found-metadata/no-page/page.tsx
new file mode 100644
index 0000000000..e3f0dd632d
--- /dev/null
+++ b/test/e2e/app-dir/parallel-route-not-found/app/@bar/not-found-metadata/no-page/page.tsx
@@ -0,0 +1,3 @@
+export default function Page() {
+ return
@bar slot
+}
diff --git a/test/e2e/app-dir/parallel-route-not-found/app/@bar/not-found-metadata/page-error/page.tsx b/test/e2e/app-dir/parallel-route-not-found/app/@bar/not-found-metadata/page-error/page.tsx
new file mode 100644
index 0000000000..e3f0dd632d
--- /dev/null
+++ b/test/e2e/app-dir/parallel-route-not-found/app/@bar/not-found-metadata/page-error/page.tsx
@@ -0,0 +1,3 @@
+export default function Page() {
+ return @bar slot
+}
diff --git a/test/e2e/app-dir/parallel-route-not-found/app/@bar/not-found-metadata/slot-error/page.tsx b/test/e2e/app-dir/parallel-route-not-found/app/@bar/not-found-metadata/slot-error/page.tsx
new file mode 100644
index 0000000000..9de1854da0
--- /dev/null
+++ b/test/e2e/app-dir/parallel-route-not-found/app/@bar/not-found-metadata/slot-error/page.tsx
@@ -0,0 +1,9 @@
+import { notFound } from 'next/navigation'
+
+export function generateMetadata() {
+ notFound()
+}
+
+export default function Page() {
+ return @bar slot
+}
diff --git a/test/e2e/app-dir/parallel-route-not-found/app/@foo/not-found-metadata/no-page/page.tsx b/test/e2e/app-dir/parallel-route-not-found/app/@foo/not-found-metadata/no-page/page.tsx
new file mode 100644
index 0000000000..bb86c7ceeb
--- /dev/null
+++ b/test/e2e/app-dir/parallel-route-not-found/app/@foo/not-found-metadata/no-page/page.tsx
@@ -0,0 +1,3 @@
+export default function Page() {
+ return @foo slot
+}
diff --git a/test/e2e/app-dir/parallel-route-not-found/app/@foo/not-found-metadata/page-error/page.tsx b/test/e2e/app-dir/parallel-route-not-found/app/@foo/not-found-metadata/page-error/page.tsx
new file mode 100644
index 0000000000..9d7c3eb678
--- /dev/null
+++ b/test/e2e/app-dir/parallel-route-not-found/app/@foo/not-found-metadata/page-error/page.tsx
@@ -0,0 +1,9 @@
+export function generateMetadata() {
+ return {
+ title: 'Create Next App',
+ }
+}
+
+export default function Page() {
+ return @foo slot
+}
diff --git a/test/e2e/app-dir/parallel-route-not-found/app/@foo/not-found-metadata/slot-error/page.tsx b/test/e2e/app-dir/parallel-route-not-found/app/@foo/not-found-metadata/slot-error/page.tsx
new file mode 100644
index 0000000000..bb86c7ceeb
--- /dev/null
+++ b/test/e2e/app-dir/parallel-route-not-found/app/@foo/not-found-metadata/slot-error/page.tsx
@@ -0,0 +1,3 @@
+export default function Page() {
+ return @foo slot
+}
diff --git a/test/e2e/app-dir/parallel-route-not-found/app/not-found-metadata/no-page/@foobar/page.tsx b/test/e2e/app-dir/parallel-route-not-found/app/not-found-metadata/no-page/@foobar/page.tsx
new file mode 100644
index 0000000000..b4f79715e6
--- /dev/null
+++ b/test/e2e/app-dir/parallel-route-not-found/app/not-found-metadata/no-page/@foobar/page.tsx
@@ -0,0 +1,9 @@
+import { notFound } from 'next/navigation'
+
+export function generateMetadata() {
+ notFound()
+}
+
+export default function Page() {
+ return @foobar slot
+}
diff --git a/test/e2e/app-dir/parallel-route-not-found/app/not-found-metadata/no-page/layout.tsx b/test/e2e/app-dir/parallel-route-not-found/app/not-found-metadata/no-page/layout.tsx
new file mode 100644
index 0000000000..3729d5322e
--- /dev/null
+++ b/test/e2e/app-dir/parallel-route-not-found/app/not-found-metadata/no-page/layout.tsx
@@ -0,0 +1,3 @@
+export default function Layout(props: { foobar: React.ReactNode }) {
+ return <>{props.foobar}>
+}
diff --git a/test/e2e/app-dir/parallel-route-not-found/app/not-found-metadata/not-found.tsx b/test/e2e/app-dir/parallel-route-not-found/app/not-found-metadata/not-found.tsx
new file mode 100644
index 0000000000..465faa0423
--- /dev/null
+++ b/test/e2e/app-dir/parallel-route-not-found/app/not-found-metadata/not-found.tsx
@@ -0,0 +1,3 @@
+export default function Page() {
+ return Custom Not Found!
+}
diff --git a/test/e2e/app-dir/parallel-route-not-found/app/not-found-metadata/page-error/page.tsx b/test/e2e/app-dir/parallel-route-not-found/app/not-found-metadata/page-error/page.tsx
new file mode 100644
index 0000000000..cfe8367402
--- /dev/null
+++ b/test/e2e/app-dir/parallel-route-not-found/app/not-found-metadata/page-error/page.tsx
@@ -0,0 +1,9 @@
+import { notFound } from 'next/navigation'
+
+export function generateMetadata() {
+ notFound()
+}
+
+export default function Page() {
+ return Hello from Page
+}
diff --git a/test/e2e/app-dir/parallel-route-not-found/app/not-found-metadata/slot-error/page.tsx b/test/e2e/app-dir/parallel-route-not-found/app/not-found-metadata/slot-error/page.tsx
new file mode 100644
index 0000000000..d72946b184
--- /dev/null
+++ b/test/e2e/app-dir/parallel-route-not-found/app/not-found-metadata/slot-error/page.tsx
@@ -0,0 +1,9 @@
+export function generateMetadata() {
+ return {
+ title: 'Create Next App',
+ }
+}
+
+export default function Page() {
+ return Hello from Page
+}
diff --git a/test/e2e/app-dir/parallel-route-not-found/parallel-route-not-found.test.ts b/test/e2e/app-dir/parallel-route-not-found/parallel-route-not-found.test.ts
index 2bdf6da4ed..ca6454d297 100644
--- a/test/e2e/app-dir/parallel-route-not-found/parallel-route-not-found.test.ts
+++ b/test/e2e/app-dir/parallel-route-not-found/parallel-route-not-found.test.ts
@@ -57,6 +57,36 @@ describe('parallel-route-not-found', () => {
expect(warnings.length).toBe(0)
})
+ it('should handle `notFound()` in generateMetadata on a page that also renders a parallel route', async () => {
+ const browser = await next.browser('/not-found-metadata/page-error')
+
+ // The page's `generateMetadata` function threw a `notFound()` error,
+ // so we should see the not found page.
+ expect(await browser.elementByCss('body').text()).toMatch(
+ /This page could not be found/
+ )
+ })
+
+ it('should handle `notFound()` in a slot', async () => {
+ const browser = await next.browser('/not-found-metadata/slot-error')
+
+ // The page's `generateMetadata` function threw a `notFound()` error,
+ // so we should see the not found page.
+ expect(await browser.elementByCss('body').text()).toMatch(
+ /This page could not be found/
+ )
+ })
+
+ it('should handle `notFound()` in a slot with no `children` slot', async () => {
+ const browser = await next.browser('/not-found-metadata/no-page')
+
+ // The page's `generateMetadata` function threw a `notFound()` error,
+ // so we should see the not found page.
+ expect(await browser.elementByCss('body').text()).toMatch(
+ /This page could not be found/
+ )
+ })
+
if (isNextDev) {
it('should not log any warnings for a regular not found page', async () => {
const browser = await next.browser('/this-page-doesnt-exist')