Skip to content

Next.js error

redirect() Does Nothing Inside a try/catch

The error

Error: NEXT_REDIRECT

redirect() 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.

Related questions

Is NEXT_REDIRECT a real error?
No. It is the control-flow signal redirect() uses to unwind the render, and the framework catches it. Seeing it in your logs means your code caught it first.
Does notFound() behave the same way?
Yes, with NEXT_NOT_FOUND. Every fix on this page applies to it unchanged.