Next.js error
redirect() Does Nothing Inside a try/catch
The error
Error: NEXT_REDIRECTredirect() works by throwing. A try/catch around it swallows the redirect and turns it into an error you logged - the navigation silently never happens.
redirect() does not return. It throws a special error that Next.js catches
higher up and converts into a navigation. That design is what lets it work
from deep inside a component tree without every caller having to return
something.
It also means a try/catch between your call and the framework will catch it
first - and then the redirect does not happen. Usually there is no visible
failure at all: the page renders as though nothing was requested, and your
error log gains an entry called NEXT_REDIRECT.
The shape that breaks
'use server';
export async function createPost(formData: FormData) {
try {
const post = await db.post.create({ /* ... */ });
redirect(`/posts/${post.id}`); // throws
} catch (error) {
console.error(error); // catches the redirect
return { error: 'Something went wrong' };
}
}The post is created. The user stays on the form, sees a generic error, and often submits again - so the visible symptom is duplicate records, which is rarely traced back to this.
The fix
Call redirect() after the try/catch, not inside it:
'use server';
export async function createPost(formData: FormData) {
let id: string;
try {
const post = await db.post.create({ /* ... */ });
id = post.id;
} catch (error) {
console.error(error);
return { error: 'Could not create the post' };
}
redirect(`/posts/${id}`); // outside the catch, so it unwinds cleanly
}The rule is simple enough to apply everywhere: the try block contains the work that can fail; the redirect happens after it succeeds.
If you must catch broadly
Some codebases wrap everything in error handling by convention. Re-throw the framework's signals rather than special-casing each call site:
import { isRedirectError } from 'next/dist/client/components/redirect-error';
try {
// ...
} catch (error) {
if (isRedirectError(error)) throw error;
// handle real failures
}That import path is internal and has moved between versions, so a version-proof
alternative is to check the digest yourself: the thrown object carries a
digest string beginning NEXT_REDIRECT (and NEXT_NOT_FOUND for
notFound()). Re-throw anything matching those and handle the rest.
The other place it appears
The same mechanism runs in route handlers and in Server Components, so the
same trap exists in a page.tsx that guards an auth check inside a try/catch.
Anywhere redirect() or notFound() sits inside one, it is not doing what it
looks like it is doing.
If Server Actions are new in your codebase, this is one of the three things people get wrong about them - the other two are also silent.
