next.js: Error: NEXT_REDIRECT while using server actions

Verify canary release

  • I verified that the issue exists in the latest Next.js canary release

Provide environment information

Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 22.4.0: Mon Mar  6 21:01:02 PST 2023; root:xnu-8796.101.5~3/RELEASE_ARM64_T8112
Binaries:
  Node: 19.2.0
  npm: 9.6.2
  Yarn: 1.22.19
  pnpm: 8.4.0
Relevant packages:
  next: 13.4.1-canary.2
  eslint-config-next: 13.4.0
  react: 18.2.0
  react-dom: 18.2.0

Which area(s) of Next.js are affected? (leave empty if unsure)

App directory (appDir: true)

Link to the code that reproduces this issue

https://github.com/ghoshnirmalya/the-fullstack-app/blob/next-server-actions/src/components/admin/Forum/ForumCreateForm.tsx#L20-L68

To Reproduce

If the server actions don’t use a try...catch block, then there is no error. The form action works as expected. However, if there is a try...catch block, then the error Error: NEXT_REDIRECT shows up.

Note that I’m using Prisma and the data gets saved before calling redirect(/admin/forums/${forum.id});. The forum object has the correct data.

Describe the Bug

The following code works as expected:

const handleSubmit = async (formData: FormData) => {
  "use server";

  const session = await getServerSession(authOptions);

  if (!session) {
    throw new Error("Unauthorized");
  }

  const forum = await prisma.forum.create({
    data: {
      title: String(formData.get("title")),
      description: String(formData.get("description")),
      creatorId: session.user.id,
    },
  });

  redirect(`/admin/forums//${forum.id}`);
}

However, if I use a try...catch block, then the following error showing up: Screenshot 2023-05-05 at 5 36 20 PM

The code that throws the above error is as follows:

const handleSubmit = async (formData: FormData) => {
  "use server";

  try {
    const session = await getServerSession(authOptions);

    if (!session) {
      throw new Error("Unauthorized");
    }

    const data = Object.fromEntries(formData.entries());

    const { title, description } = forumCommentCreateSchema.parse(data);

    const forum = await prisma.forum.create({
      data: {
        title,
        description,
        creatorId: session.user.id,
      },
    });

    redirect(`/admin/forums/${forum.id}`);
  } catch (error) {
    if (error instanceof z.ZodError) {
      throw new Error(JSON.stringify(error.issues));
    }

    throw new Error("Something went wrong.");
  }
};

Expected Behavior

The redirect() function should redirect to the correct URL instead of throwing an error.

Which browser are you using? (if relevant)

Chromium Engine Version 112.0.5615.137

How are you deploying your application? (if relevant)

Vercel

About this issue

  • Original URL
  • State: closed
  • Created a year ago
  • Reactions: 11
  • Comments: 29 (5 by maintainers)

Most upvoted comments

I noticed a behaviour that’s not really intuitive at first:

→ The redirect() only works if my server action call is returned in the startTransition() callback.

Otherwise said:

This works

startTransition(() => {
  return updateRecipientInfo(values);
});

This breaks with a NEXT_REDIRECT error in the console, and no redirection happening.

startTransition(() => {
  updateRecipientInfo(values);
});

I believe this is the source for your error @JoseVSeb.

Is this behaviour expected @timneutkens ?

As others have pointed out the way redirect() and notFound() work is by throwing an error that Next.js can handle in order to stop execution of the code.

 const handleEdit = async (formData: FormData) => {
  "use server";

  try {
    await update({
      id: Number(forum.id),
      title: String(formData.get("title")),
      content: String(formData.get("content")),
    });

    revalidatePath(`/admin/forums/${forum.id}`);
    redirect(`/admin/forums`);
  } catch (error) {
    // you end up catching the `redirect()` above in this case.
    console.log(error);
  }
};

One way to fix it:

 const handleEdit = async (formData: FormData) => {
  "use server";

  try {
    await update({
      id: Number(forum.id),
      title: String(formData.get("title")),
      content: String(formData.get("content")),
    });
  }  catch (error) {
    console.log(error);
    // Re-throw the error, that way `revalidatePath` / `redirect` are not executed.
    throw error
  }

  revalidatePath(`/admin/forums/${forum.id}`);
  redirect(`/admin/forums`);
};

We’ll make sure the docs are updated to explain redirect() and notFound() use error throwing under the hood.

Assuming updateRecipientInfo is a server action, the code provided is a dangling promise, whereas when you return it’s an awaited promise.

startTransition(() => {
  const value = updateRecipientInfo(values);
  // `value` is a promise. It's not awaited/then'ed so it ends up throwing in the global scope.
});

The right way to go about it is this:

startTransition(async () => {
  await updateRecipientInfo(values);
});

In the experimental version of React that is enabled when you enable server actions async transitions are supported (it’s what action uses under the hood too.

Hope that clarifies why it would not be caught when you “just call it” instead of returning. It’s similar to not awaiting any other async function

I ran into the same issue and this fixes it, but it throws a type error. A transition function can only return void or undefined, and returning a Promise<void> breaks that.

Thanks a lot for your answer 🙂

I didn’t realize an error in a dangling promise couldn’t be caught, and by reflex I used this the same way the nextjs documentation uses router.refresh() in startTransition, although that’s not the same thing at all.

Maybe an uncaught NEXT_REDIRECT would benefit from a more helpful error message? (I believe lots of newcomers will get bitten by that error).

You have to place the redirect() outside try-catch. Internally, redirect() is throwing an Error object (see https://github.com/vercel/next.js/blob/canary/packages/next/src/client/components/redirect.ts#L12-L36), which is also why it’s typed as function redirect(...): never.

Try to place less code in a try-catch anyway. You’ll get the same behaviour of throw new Error("Unauthorized");.

If you have a global error handler/wrapper, you can rethrow it with something like:

} catch (err) {
  if(isRedirectError(err)) throw err;

  // ...
}

This sounds like something an eslint rule should be able to handle well

Hey folks,

I’m encountering the same issue with the latest release 13.4.3 something as simple as this:

const Login = async (email: string, password: string) => {
    'use server';
    const res = await fetch(`https://myurl.com/login`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        email,
        password,
      }),
      cache: 'no-store',
    });
    const { error, data } = await res.json();
    if (error === false) cookies().set('accessToken', data.accessToken);
    redirect('/dashboard');
  };

Gives me the same redirect error as the OP, what am i missing?

I removed the try-catch from my code and now the redirect works, thanks

A transition function can only return void or undefined

This is actually not the case when you enable server actions. The React experimental channel has support for Async Transitions, so an async function is supported in that case.

this is my server-side code:

class AdmissionService {
  @Validate
  async initiate(@YupSchema(StudentSchema) data: StudentSchema) {
    const userId = "8b896311-e958-4ece-872c-aac11a497dea";
    const admission = await prisma.workflow.create({
      data: {
        data,
        type: "ADMISSION",
        requestedById: userId,
        status: "PENDING",
      },
      select: { id: true },
    });
    redirect(`/workflows/admission/${admission.id}`);
  }
}

I have confirmed that the x-action-redirect header is in the response with the correct redirect path /workflows/admission/d65a2be3-bf7d-4514-8606-8815295767a8. redirection fails in client side. image

image image image

I removed try catch and works idk why but its works

You can use transition hook to wrapped the server action, it looks like this: CleanShot 2023-05-12 at 10 11 21

Can you explain what the purpose of useTransition is here? I’ve seen it used with server actions a couple times but I don’t get the point!

@benjaminwaterlot thanks, that was the issue. Although, i don’t understand why that’s needed. useTransition hook merely provides a pending state for promise resolution. Is server action using the hook to somehow detect redirection?

You can use transition hook to wrapped the server action, it looks like this: CleanShot 2023-05-12 at 10 11 21