unstable_rethrow()

Re-throws Next.js internal control-flow errors so a catch block does not swallow them.

Signature
unstable_rethrow(error: unknown): void

Parameters

ParameterTypeDescription
$errorunknownThe caught error object. Pass it as the only argument, on the first line of the catch block or inside a promise .catch handler.

Return value

voidReturns nothing when the error is an ordinary application error, letting your own handling continue. When the error is a Next.js internal control-flow error it re-throws instead, and never returns.

Errors

Re-throws the error it was given if it is a Next.js internal control-flow error — those raised by notFound(), redirect(), permanentRedirect(), and by Request-time APIs in a segment required to stay static.

Several Next.js APIs signal by throwing. notFound(), redirect() and permanentRedirect() all raise an internal error the framework expects to catch at the segment boundary. A broad catch in your own code intercepts it first, and the 404 or the navigation quietly disappears — usually surfacing as a blank page or a stray console.error.

unstable_rethrow(err) on the first line of the catch block sorts the two apart: framework errors are re-thrown and continue on their way, application errors fall through so your handling runs as written.

What else it protects

Request-time APIs throw the same way when a segment is required to stay static — cookies(), headers(), searchParams, and fetch with cache: 'no-store' or next: { revalidate: 0 }. Partial Prerendering affects this too.

You can often avoid it entirely by narrowing the try to the fallible call and letting the caller handle the exception. Reach for it when a single catch really can see both kinds. Note the ordering rule: cleanup such as clearing timers must happen before the call or in a finally block, because the re-throw does not return.

Example

Example · typescript
import { notFound, unstable_rethrow } from 'next/navigation'

export default async function Page() {
  try {
    const post = await fetch('https://api.example.com/posts/1').then((res) => {
      if (res.status === 404) notFound()
      if (!res.ok) throw new Error(res.statusText)
      return res.json()
    })

    return <article>{post.title}</article>
  } catch (err) {
    unstable_rethrow(err)
    console.error(err)
    return <p>Could not load this post.</p>
  }
}

When to use it

  • Keep notFound() working inside a data-fetching helper that already has its own try/catch
  • Log unexpected errors in a Server Component without breaking redirect()
  • Add retry or fallback logic around a fetch that may also call notFound()
  • Wrap a third-party client whose errors you handle, in a component that also redirects

Frequently asked questions

Why does try/catch break notFound() and redirect()?

Both APIs work by throwing an internal error that Next.js is meant to catch at the route segment boundary, so your own catch block intercepts it first and cancels the behaviour. Call unstable_rethrow(err) as the first statement in the catch to pass framework errors back through.

Where do I call unstable_rethrow?

On the very first line of the catch block, or at the top of a promise's .catch handler, with the caught error as its only argument. It re-throws framework errors without returning, so anything after it is skipped, and cleanup has to run before the call or move into a finally block.

Do I always need unstable_rethrow?

No. It is only needed when one catch block can see both application errors and framework interrupts. Narrowing the try to just the fallible call, or letting the caller handle the exception, usually removes the need for it.

More examples

What goes wrong without it

The catch block treats the framework interrupt as an ordinary failure. Instead of not-found.js and a 404, the visitor gets the generic "Could not load this post" message and the server logs NEXT_HTTP_ERROR_FALLBACK;404 as if it were a bug. Adding unstable_rethrow(err) as the first line of the catch restores the intended behaviour.

Example · typescript
// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation'

export default async function Page() {
  try {
    const post = await fetch('https://api.example.com/posts/1').then((res) => {
      if (res.status === 404) notFound()
      if (!res.ok) throw new Error(res.statusText)
      return res.json()
    })

    return <article>{post.title}</article>
  } catch (err) {
    // Catches NEXT_HTTP_ERROR_FALLBACK;404 as well as real failures.
    console.error(err)
    return <p>Could not load this post.</p>
  }
}

Cleanup belongs in finally, not before the rethrow

unstable_rethrow re-throws without returning, so any statement placed after it in the catch is skipped for framework errors. clearTimeout sits in finally, which runs on every exit path — including the re-thrown 404 — so the timer is never left dangling.

Example · typescript
// app/reports/page.tsx
import { notFound, unstable_rethrow } from 'next/navigation'
import { getReport } from '@/lib/reports'

export default async function ReportPage() {
  const controller = new AbortController()
  const timer = setTimeout(() => controller.abort(), 5000)

  try {
    const report = await getReport({ signal: controller.signal })
    if (!report) notFound()
    return <pre>{report.body}</pre>
  } catch (err) {
    unstable_rethrow(err)
    return <p>Report unavailable.</p>
  } finally {
    clearTimeout(timer)
  }
}

Using it inside a promise .catch handler

The same rule applies to a .catch handler as to a catch block: call it first. A real network failure falls through to the placeholder object, while the 404 interrupt is re-thrown and reaches the not-found boundary. Note that the caller must await this function — an interrupt thrown from an un-awaited promise has nothing listening for it.

Example · typescript
// app/lib/posts.ts
import { notFound, unstable_rethrow } from 'next/navigation'

export async function loadPost(slug: string) {
  return fetch(`https://api.example.com/posts/${slug}`)
    .then((res) => {
      if (res.status === 404) notFound()
      if (!res.ok) throw new Error(res.statusText)
      return res.json()
    })
    .catch((err) => {
      unstable_rethrow(err)
      return { title: 'Draft unavailable', content: '' }
    })
}

Discussion

  • Be the first to comment on this lesson.