DEV Community

Anas Sheikh
Anas Sheikh

Posted on

Your Next.js redirect() Silently Does Nothing Because It's Inside a try/catch

A user fills in your form and clicks "Create post". The post is saved in the database. And the page shows "Could not create post".

They try again. Now there are two posts.

Nothing is wrong with your database. Your error handling caught something it was never supposed to catch.

The Code That Causes It

// app/actions.ts
'use server';

import { redirect } from 'next/navigation';

export async function createPost(formData: FormData) {
  try {
    const post = await db.post.create({
      data: { title: String(formData.get('title')) },
    });

    redirect(`/posts/${post.id}`);
  } catch (err) {
    return { error: 'Could not create post' };
  }
}
Enter fullscreen mode Exit fullscreen mode

It reads perfectly. Save the post, go to its page, and if anything fails, return an error. A tidy, defensive pattern that you have probably written a hundred times in other frameworks.

Why It Breaks

In Next.js, redirect() does not return. It throws a special error, and the framework catches that error higher up and turns it into a redirect response.

That means your catch block receives it first. Your code swallows the redirect, returns { error: ... }, and the framework never sees it.

The same is true for notFound(), which also works by throwing. Both are written to stop execution immediately, which is why TypeScript treats them as functions that never return.

Why It Is Hard to Catch

  • The data is saved. The only visible symptom is a wrong error message, so it looks like a UI bug, not a logic bug.
  • It is inconsistent in your head. The same redirect() works fine outside a try, so you assume it is fine everywhere.
  • Your logs are confusing. If you log the caught error, you will see a strange error with a NEXT_REDIRECT digest and think something crashed.
  • It tempts you to retry. Users see an error, submit again and create duplicates.

Fix 1: Keep redirect() Outside the try Block

Only wrap the code that can genuinely fail, and redirect afterwards:

export async function createPost(formData: FormData) {
  let postId: string;

  try {
    const post = await db.post.create({
      data: { title: String(formData.get('title')) },
    });
    postId = post.id;
  } catch (err) {
    return { error: 'Could not create post' };
  }

  redirect(`/posts/${postId}`);
}
Enter fullscreen mode Exit fullscreen mode

The try now covers only the database call. If it fails, you return the error. If it succeeds, execution continues to redirect(), which is outside any catch, so it can do its job.

This is the cleanest option, and it is the one I use by default.

Fix 2: Rethrow If You Must Catch Everything

Sometimes the redirect() sits deep inside a helper, and restructuring is awkward. Then let the framework's own errors pass through your catch. Newer Next.js versions provide a helper for exactly this:

import { redirect, unstable_rethrow } from 'next/navigation';

try {
  // ...work that may call redirect() or notFound() somewhere inside
} catch (err) {
  unstable_rethrow(err); // lets redirect/notFound errors continue upward
  return { error: 'Something went wrong' };
}
Enter fullscreen mode Exit fullscreen mode

The name says it is not final, so check the documentation for the version you use. If it is not available in yours, structure your code as in Fix 1.

Fix 3: Do Not Use Catch-All to Control Flow

A broader habit helps here. A catch (err) with no check is a very wide net. Catch what you expect:

catch (err) {
  if (err instanceof DuplicateTitleError) {
    return { error: 'You already have a post with that title' };
  }
  throw err; // anything else is not yours to handle here
}
Enter fullscreen mode Exit fullscreen mode

Rethrowing what you do not recognize means the framework's own signals, real crashes and bugs all reach the place that is meant to handle them.

Where Else This Bites

  • notFound() inside try. A missing record becomes your generic error instead of a 404 page.
  • Route handlers and Server Components. The same rule applies: any try/catch around a call that can redirect or 404 will swallow it.
  • .catch() on a promise. somePromise.catch(() => null) has the same effect if the promise's code calls redirect().
  • Wrapper functions. A helper like withErrorHandling(fn) that catches everything will break redirects in every function it wraps.

Quick Checklist

Situation What to do
redirect() after a database write Put the write in try, redirect() after it
notFound() when a record is missing Call it outside the try, or rethrow
A shared error-handling wrapper Make it rethrow framework errors
Unsure Catch specific error types, rethrow the rest

The Rule

redirect() and notFound() signal by throwing, so a catch around them will eat the signal. Keep them outside try blocks, or rethrow what you do not recognize.

Quick audit: search your project for redirect( and notFound(. For each hit, look upward. If it sits inside a try, you have either a bug now or one waiting for the day the code path is used.

If you want a Next.js starting point where forms and actions are set up sensibly from day one, you can browse my templates on pixelanas.com.

Get the templates: https://pixelanas.gumroad.com

Have you ever shipped a form that showed an error after it had succeeded? Tell me in the comments how long it took you to find it.


Anas, full-stack Next.js developer building SaaS products and premium templates. X: @ASheikh69751

Top comments (2)

Collapse
 
mercerk17 profile image
Kai Mercer •

This matches the Next.js docs: the redirect page says redirect throws, so call it outside the try block, and the unstable_rethrow page lists notFound(), redirect() and permanentRedirect() as errors that should be rethrown (I only read the docs here, I did not run your example).

One thing worth adding for the duplicate-post symptom: even after the fix, a double click or a retry after a slow response can still create two rows. A unique key from the form (a hidden id generated when the form renders, with a unique constraint on it) makes the second submit a no-op. And per the docs, unstable_rethrow(err) goes at the top of the catch block, before any logging or returning, so the framework error is not turned into your generic message first.

Collapse
 
anas_sheikh_2 profile image
Anas Sheikh •

Great point! Even after fixing the redirect issue, duplicate submissions can still happen if users double-click or retry after a slow response. Using an idempotency key with a unique database constraint is a good way to prevent duplicate posts. Also, unstable_rethrow(err) should be called at the start of the catch block so Next.js can handle its redirect and not-found errors correctly.