Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement unstable_rethrow #65831

Merged
merged 5 commits into from
May 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions docs/02-app/02-api-reference/04-functions/unstable_rethrow.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
---
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)
ztanner marked this conversation as resolved.
Show resolved Hide resolved
- `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.
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
1 change: 1 addition & 0 deletions packages/next/src/client/components/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,4 +269,5 @@ export {
permanentRedirect,
RedirectType,
ReadonlyURLSearchParams,
unstable_rethrow,
} from './navigation.react-server'
26 changes: 26 additions & 0 deletions packages/next/src/client/components/unstable-rethrow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
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.
*
* Read more: [Next.js Docs: `unstable_rethrow`](https://nextjs.org/docs/app/api-reference/functions/unstable_rethrow)
*/
export function unstable_rethrow(error: unknown): void {
if (
isNextRouterError(error) ||
isBailoutToCSRError(error) ||
isDynamicUsageError(error) ||
isPostpone(error)
) {
throw error
}

if (error instanceof Error && 'cause' in error) {
unstable_rethrow(error.cause)
}
}
22 changes: 22 additions & 0 deletions test/e2e/app-dir/unstable-rethrow/app/cause/page.tsx
Original file line number Diff line number Diff line change
@@ -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 <p>hello world</p>
}
14 changes: 14 additions & 0 deletions test/e2e/app-dir/unstable-rethrow/app/dynamic-error/page.tsx
Original file line number Diff line number Diff line change
@@ -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)
}

return <p>hello world</p>
}
7 changes: 7 additions & 0 deletions test/e2e/app-dir/unstable-rethrow/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function Root({ children }: { children: React.ReactNode }) {
return (
<html>
<body>{children}</body>
</html>
)
}
13 changes: 13 additions & 0 deletions test/e2e/app-dir/unstable-rethrow/app/not-found-page/page.tsx
Original file line number Diff line number Diff line change
@@ -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)
}

return <p>hello world</p>
}
12 changes: 12 additions & 0 deletions test/e2e/app-dir/unstable-rethrow/app/page.tsx
Original file line number Diff line number Diff line change
@@ -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)
}

return <p>hello world</p>
}
11 changes: 11 additions & 0 deletions test/e2e/app-dir/unstable-rethrow/app/redirect/page.tsx
Original file line number Diff line number Diff line change
@@ -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)
}
}
6 changes: 6 additions & 0 deletions test/e2e/app-dir/unstable-rethrow/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {}

module.exports = nextConfig
30 changes: 30 additions & 0 deletions test/e2e/app-dir/unstable-rethrow/unstable-rethrow.test.ts
Original file line number Diff line number Diff line change
@@ -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')
})
}
})
Loading