From 5fa80a210b6a18c4c300949ffdfa2a8ed8cc74a0 Mon Sep 17 00:00:00 2001 From: Zack Tanner <1939140+ztanner@users.noreply.github.com> Date: Wed, 15 May 2024 18:01:45 -0700 Subject: [PATCH 1/4] implement unstable_rethrow --- .../04-functions/unstable_rethrow.mdx | 62 +++++++++++++++++++ .../components/navigation.react-server.ts | 1 + .../next/src/client/components/navigation.ts | 1 + .../src/client/components/unstable-rethrow.ts | 34 ++++++++++ .../unstable-rethrow/app/cause/page.tsx | 22 +++++++ .../app/dynamic-error/page.tsx | 14 +++++ .../app-dir/unstable-rethrow/app/layout.tsx | 7 +++ .../app/not-found-page/page.tsx | 13 ++++ .../e2e/app-dir/unstable-rethrow/app/page.tsx | 12 ++++ .../unstable-rethrow/app/redirect/page.tsx | 11 ++++ .../app-dir/unstable-rethrow/next.config.js | 6 ++ .../unstable-rethrow/unstable-rethrow.test.ts | 30 +++++++++ 12 files changed, 213 insertions(+) create mode 100644 docs/02-app/02-api-reference/04-functions/unstable_rethrow.mdx create mode 100644 packages/next/src/client/components/unstable-rethrow.ts create mode 100644 test/e2e/app-dir/unstable-rethrow/app/cause/page.tsx create mode 100644 test/e2e/app-dir/unstable-rethrow/app/dynamic-error/page.tsx create mode 100644 test/e2e/app-dir/unstable-rethrow/app/layout.tsx create mode 100644 test/e2e/app-dir/unstable-rethrow/app/not-found-page/page.tsx create mode 100644 test/e2e/app-dir/unstable-rethrow/app/page.tsx create mode 100644 test/e2e/app-dir/unstable-rethrow/app/redirect/page.tsx create mode 100644 test/e2e/app-dir/unstable-rethrow/next.config.js create mode 100644 test/e2e/app-dir/unstable-rethrow/unstable-rethrow.test.ts diff --git a/docs/02-app/02-api-reference/04-functions/unstable_rethrow.mdx b/docs/02-app/02-api-reference/04-functions/unstable_rethrow.mdx new file mode 100644 index 0000000000000..643504177b11a --- /dev/null +++ b/docs/02-app/02-api-reference/04-functions/unstable_rethrow.mdx @@ -0,0 +1,62 @@ +--- +title: unstable_rethrow +description: API Reference for the unstable_rethrow function. +--- + +`unstable_rethrow` can be used to avoid catching internal errors thrown by Next.js when attempting to handle errors thrown from your own application. + +For example, given the following code: + +```jsx +import { notFound } from 'next/navigation' + +export default async function Component() { + try { + notFound() + await getData() + } catch (err) { + console.error(err) + } +} +``` + +In this scenario, `console.error` will pick up the error thrown by `notFound`, which won't be useful for logging and will prevent +the `not-found.js` page from rendering. Instead, you can do this: + +```jsx +import { notFound, unstable_rethrow } from 'next/navigation' + +export default async function Component() { + try { + notFound() + await getData() + } catch (err) { + unstable_rethrow(err) + console.error(err) + } +} +``` + +If the caught error is meant to be handled by Next.js, `unstable_rethrow` will re-throw the error instead of letting it reach your +error handling code. + +The following Next.js APIs rely on throwing an error which should be rethrown and handled by Next.js itself: + +- [`notFound()`](/docs/app/api-reference/functions/not-found) +- [`redirect()`](/docs/app/building-your-application/routing/redirecting#redirect-function) +- [`permanentRedirect()`](/docs/app/building-your-application/routing/redirecting#permanentredirect-function) +- + +If a route segment is marked to throw an error unless it's static, a dynamic function call will also throw an error that should similarly not be caught by the developer. Note that Partial Prerendering (PPR) affects this behavior as well. These APIs are: + +- [`cookies()`](/docs/app/api-reference/functions/cookies) +- [`headers()`](/docs/app/api-reference/functions/headers) +- [`searchParams`](/docs/app/api-reference/file-conventions/page#searchparams-optional) +- `fetch(..., { cache: 'no-store' })` +- `fetch(..., { next: { revalidate: 0 } })` + +> **Good to know**: +> +> - This method should be called at the top of the catch block, passing the error object as its only argument. It can also be used within a .catch handler of a promise. +> - If you ensure that your calls to APIs that throw are not wrapped in a try/catch then you don't need to use `unstable_rethrow` +> - Any resource cleanup (like clearing intervals, timers, etc) would have to either happen prior to the call to `unstable_rethrow` or within a `finally` block. diff --git a/packages/next/src/client/components/navigation.react-server.ts b/packages/next/src/client/components/navigation.react-server.ts index 3dc6eaeb551bc..79644ff64773e 100644 --- a/packages/next/src/client/components/navigation.react-server.ts +++ b/packages/next/src/client/components/navigation.react-server.ts @@ -28,4 +28,5 @@ class ReadonlyURLSearchParams extends URLSearchParams { export { redirect, permanentRedirect, RedirectType } from './redirect' export { notFound } from './not-found' +export { unstable_rethrow } from './unstable-rethrow' export { ReadonlyURLSearchParams } diff --git a/packages/next/src/client/components/navigation.ts b/packages/next/src/client/components/navigation.ts index 5d99af79e9239..370ea4c24e346 100644 --- a/packages/next/src/client/components/navigation.ts +++ b/packages/next/src/client/components/navigation.ts @@ -269,4 +269,5 @@ export { permanentRedirect, RedirectType, ReadonlyURLSearchParams, + unstable_rethrow, } from './navigation.react-server' diff --git a/packages/next/src/client/components/unstable-rethrow.ts b/packages/next/src/client/components/unstable-rethrow.ts new file mode 100644 index 0000000000000..4055655a24df0 --- /dev/null +++ b/packages/next/src/client/components/unstable-rethrow.ts @@ -0,0 +1,34 @@ +import { isDynamicUsageError } from '../../export/helpers/is-dynamic-usage-error' +import { isPostpone } from '../../server/lib/router-utils/is-postpone' +import { isBailoutToCSRError } from '../../shared/lib/lazy-dynamic/bailout-to-csr' +import { isNextRouterError } from './is-next-router-error' + +/** + * This function should be used to rethrow internal Next.js errors so that they can be handled by the framework. + * When wrapping an API that uses errors to interrupt control flow, you should use this function before you do any error handling. + * This function will rethrow the error if it is a Next.js error so it can be handled, otherwise it will do nothing. + * + * APIs that use errors to interrupt control flow include: + * - `redirect` + * - `cookies()` + * - `headers()` + * - `notFound()` + * - `fetch(..., { cache: 'no-store' })` + * - `fetch(..., { next: { revalidate: 0 } })` + * + * Read more: [Next.js Docs: `unstable_rethrow`](https://nextjs.org/docs/app/api-reference/functions/unstable_rethrow) + */ +export function unstable_rethrow(error: unknown): void | never { + if ( + isNextRouterError(error) || + isBailoutToCSRError(error) || + isDynamicUsageError(error) || + isPostpone(error) + ) { + throw error + } + + if (error instanceof Error && 'cause' in error) { + unstable_rethrow(error.cause) + } +} diff --git a/test/e2e/app-dir/unstable-rethrow/app/cause/page.tsx b/test/e2e/app-dir/unstable-rethrow/app/cause/page.tsx new file mode 100644 index 0000000000000..a2f959a17bd88 --- /dev/null +++ b/test/e2e/app-dir/unstable-rethrow/app/cause/page.tsx @@ -0,0 +1,22 @@ +import { cookies } from 'next/headers' +import { unstable_rethrow } from 'next/navigation' + +function someFunction() { + try { + cookies() + } catch (err) { + throw new Error('Oopsy', { cause: err }) + } +} + +export default async function Page() { + try { + someFunction() + } catch (err) { + console.log('[test assertion]: checking error') + unstable_rethrow(err) + console.error('[test assertion]: error leaked', err) + } + + return
hello world
+} diff --git a/test/e2e/app-dir/unstable-rethrow/app/dynamic-error/page.tsx b/test/e2e/app-dir/unstable-rethrow/app/dynamic-error/page.tsx new file mode 100644 index 0000000000000..2934e68fde129 --- /dev/null +++ b/test/e2e/app-dir/unstable-rethrow/app/dynamic-error/page.tsx @@ -0,0 +1,14 @@ +import { cookies } from 'next/headers' +import { unstable_rethrow } from 'next/navigation' + +export default async function Page() { + try { + cookies() + } catch (err) { + console.log('[test assertion]: checking error') + unstable_rethrow(err) + console.error('[test assertion]: error leaked', err) + } + + returnhello world
+} diff --git a/test/e2e/app-dir/unstable-rethrow/app/layout.tsx b/test/e2e/app-dir/unstable-rethrow/app/layout.tsx new file mode 100644 index 0000000000000..e7077399c03ce --- /dev/null +++ b/test/e2e/app-dir/unstable-rethrow/app/layout.tsx @@ -0,0 +1,7 @@ +export default function Root({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ) +} diff --git a/test/e2e/app-dir/unstable-rethrow/app/not-found-page/page.tsx b/test/e2e/app-dir/unstable-rethrow/app/not-found-page/page.tsx new file mode 100644 index 0000000000000..6f4f2fc5f3875 --- /dev/null +++ b/test/e2e/app-dir/unstable-rethrow/app/not-found-page/page.tsx @@ -0,0 +1,13 @@ +import { notFound, unstable_rethrow } from 'next/navigation' + +export default async function Page() { + try { + notFound() + } catch (err) { + console.log('[test assertion]: checking error') + unstable_rethrow(err) + console.error('[test assertion]: error leaked', err) + } + + returnhello world
+} diff --git a/test/e2e/app-dir/unstable-rethrow/app/page.tsx b/test/e2e/app-dir/unstable-rethrow/app/page.tsx new file mode 100644 index 0000000000000..6ccd482d56267 --- /dev/null +++ b/test/e2e/app-dir/unstable-rethrow/app/page.tsx @@ -0,0 +1,12 @@ +import { notFound, unstable_rethrow } from 'next/navigation' + +export default async function Page() { + try { + notFound() + } catch (err) { + unstable_rethrow(err) + console.error(err) + } + + returnhello world
+} diff --git a/test/e2e/app-dir/unstable-rethrow/app/redirect/page.tsx b/test/e2e/app-dir/unstable-rethrow/app/redirect/page.tsx new file mode 100644 index 0000000000000..b6df072742956 --- /dev/null +++ b/test/e2e/app-dir/unstable-rethrow/app/redirect/page.tsx @@ -0,0 +1,11 @@ +import { redirect, unstable_rethrow } from 'next/navigation' + +export default function Page() { + try { + redirect('/') + } catch (err) { + console.log('[test assertion]: checking error') + unstable_rethrow(err) + console.error('[test assertion]: error leaked', err) + } +} diff --git a/test/e2e/app-dir/unstable-rethrow/next.config.js b/test/e2e/app-dir/unstable-rethrow/next.config.js new file mode 100644 index 0000000000000..807126e4cf0bf --- /dev/null +++ b/test/e2e/app-dir/unstable-rethrow/next.config.js @@ -0,0 +1,6 @@ +/** + * @type {import('next').NextConfig} + */ +const nextConfig = {} + +module.exports = nextConfig diff --git a/test/e2e/app-dir/unstable-rethrow/unstable-rethrow.test.ts b/test/e2e/app-dir/unstable-rethrow/unstable-rethrow.test.ts new file mode 100644 index 0000000000000..1d26bbdd054db --- /dev/null +++ b/test/e2e/app-dir/unstable-rethrow/unstable-rethrow.test.ts @@ -0,0 +1,30 @@ +import { nextTestSetup } from 'e2e-utils' + +describe('unstable-rethrow', () => { + const { next, isNextStart } = nextTestSetup({ + files: __dirname, + }) + + it('should correctly trigger the not found page as not found', async () => { + const browser = await next.browser('/not-found-page') + expect(await browser.elementByCss('body').text()).toContain( + 'This page could not be found.' + ) + }) + + it('should handle an internal error that gets propagated to the `cause` field', async () => { + const browser = await next.browser('/cause') + expect(await browser.elementByCss('body').text()).toContain('hello world') + }) + + if (isNextStart) { + it('should not log any errors at build time', async () => { + expect(next.cliOutput).toContain('[test assertion]: checking error') + expect(next.cliOutput).not.toContain('[test assertion]: error leaked') + }) + + it('should correctly mark the dynamic page as dynamic', async () => { + expect(next.cliOutput).toContain('ƒ /dynamic-error') + }) + } +}) From cfc1b611d1f30ffe9e6a2ea5840d22db554f3c3d Mon Sep 17 00:00:00 2001 From: Zack Tanner <1939140+ztanner@users.noreply.github.com> Date: Thu, 16 May 2024 15:47:28 -0700 Subject: [PATCH 2/4] Update docs/02-app/02-api-reference/04-functions/unstable_rethrow.mdx Co-authored-by: JJ Kasper