noctcore-rsc/no-navigation-throw-in-try
A
next/navigationcall that works by throwing must not sit in atrywhosecatchswallows it.
Recommended preset: error · Autofix: no · Suggestions: no · Type information: not needed
In the Next.js App Router, redirect(), permanentRedirect(), notFound(), forbidden() and
unauthorized() do not return. Each throws a special error that Next.js catches further up and
turns into the redirect or the error page. A catch that handles every error catches that one
too, and the navigation silently never happens:
import { redirect } from 'next/navigation';
export async function createPost(data: FormData) { try { await db.post.create({ data: parse(data) }); // throws, and the catch below eats it: the user never leaves the form redirect('/posts'); } catch (error) { return { error: 'Could not save the post' }; }}Nothing in the types or the tests says anything is wrong: the action returns { error } for a
save that succeeded. Move the call out of the try, so only the work that can fail is guarded:
import { redirect } from 'next/navigation';
export async function createPost(data: FormData) { try { await db.post.create({ data: parse(data) }); } catch (error) { return { error: 'Could not save the post' }; } redirect('/posts');}When the call has to stay inside the try, hand Next.js its error back with
unstable_rethrow, first
in the catch:
import { notFound } from 'next/navigation';
export default async function Page({ params }: { params: { id: string } }) { try { const post = await getPost(params.id); if (!post) notFound(); return render(post); } catch (error) { return renderFallback(); }}import { notFound, unstable_rethrow } from 'next/navigation';
export default async function Page({ params }: { params: { id: string } }) { try { const post = await getPost(params.id); if (!post) notFound(); return render(post); } catch (error) { unstable_rethrow(error); return renderFallback(); }}Prior art
Section titled “Prior art”- react-doctor’s
nextjs-no-redirect-in-try-catch. - vercel/next.js#55586, the issue that led to
unstable_rethrow. @next/eslint-plugin-nexthas no equivalent rule.
What it flags
Section titled “What it flags”A call to redirect, permanentRedirect, notFound, forbidden or unauthorized that:
- resolves to an import from
next/navigation, under its own name, an alias (import { redirect as go }) or a namespace (nav.redirect()), and - sits, lexically and without a function in between, in the block of a
trythat has acatch, and - that
catchdoes not hand the caught error back.
A catch hands the error back when it calls unstable_rethrow(error) from next/navigation
with its own parameter, or throws its own parameter (throw error). A guarded rethrow such as
if (isRedirectError(error)) throw error; counts too. Wrapping it does not:
throw new Error('failed', { cause: error }) throws an error Next.js does not recognise.
import { redirect } from 'next/navigation';
function guard(user: User | null) { try { if (!user) redirect('/login'); } catch (error) { throw new Error('guard failed', { cause: error }); }}A catch that rethrows passes the error on to the next try out, so an outer catch that
swallows it is still reported.
There is no autofix: which of moving the call and rethrowing is right depends on what the try
was guarding.
What it does not flag
Section titled “What it does not flag”- A call outside any
try, or in atry/finallywith nocatch: the error propagates. - A call in the
catchor thefinallyblock: it throws out of thetrystatement. - A call inside a function that is only defined in the
try, such as a callback or an event handler: thetrydoes not run it. - A function called
redirectornotFoundthat is not thenext/navigationexport: a local function, a parameter, or an import from@remix-run/nodeor anywhere else.
import { redirect } from '@remix-run/node';
export async function action() { try { return redirect('/done'); } catch { return null; }}When not to use it
Section titled “When not to use it”It only reports calls imported from next/navigation, so outside a Next.js App Router project it has
nothing to do. Inside one, a catch that swallows a navigation is almost always a
bug.