Next.js Discord

Discord Forum

Calling notFound() in server action

Answered
Chinese Alligator posted this in #help-forum
Open in Discord
Chinese AlligatorOP
Hi!
How do you handle 404 from server actions?
Given the following code:
async function serverAction() {
  "use server";
  notFound();
}

I've expected that when I call such action, not-found.tsx page will be automatically shown.
But it does not. I just get error with message NEXT_NOT_FOUND, so I need manually handle it.
Also, this error message is not typed, so is it reliable to use?

Would appreciate your experience on that.
Answered by Chinese Alligator
I've digged more and found another workaround, inspired by this SO discussion: https://stackoverflow.com/questions/67889383/react-error-boundaries-with-useeffect-and-async-function-what-im-missing

By React docs, errors thrown in async code are not handled by error boundaries. That's why Next can't show appropriate not-found.txs (and error.tsx as well). Following the suggestion from SO, I've made it working by re-throwing the error during render:

// buttons.tsx
"use client";

import { useState } from "react";

export default function Button({ action }) {
  const [error, setError] = useState<Error | null>(null);

  // Re-throw error to be caught by error.tsx / not-found.tsx
  if (error) throw error;

  return (
    <button onClick={() => action().catch(setError)}>
      Button with server action
    </button>
  );
}
View full answer

8 Replies

@Chinese Alligator Hi! How do you handle 404 from server actions? Given the following code: ts async function serverAction() { "use server"; notFound(); } I've expected that when I call such action, `not-found.tsx` page will be automatically shown. But it does not. I just get error with message `NEXT_NOT_FOUND`, so I need manually handle it. Also, this error message is not typed, so is it reliable to use? Would appreciate your experience on that.
try to return your notfound function. Like
async function serverAction() {
  "use server";
  return notFound();
}

Else return some json that says, it wasn't found. Another thing you can do is do thrown an error and your frontend catch the error and handle it.

In both ways the redirect to the notfound page will be handled by the client
if you do wrap try/catch around serverAction, you need to rethrow the special NOT_FOUND error
import { notFound } from "next/navigation";
import { isNotFoundError } from "next/dist/client/components/not-found";

export default function Page() {
  return (
    <form
      action={async () => {
        "use server";
        try {
          notFound();
        } catch (e) {
          // This will be made easier in Next.js 15 with the unstable_rethrow API
          if (isNotFoundError(e)) throw e;
        }
      }}
    >
      <button>Click me</button>
    </form>
  );
}
@joulev i can't reproduce the issue. are you sure you don't wrap something around `serverAction`?
Chinese AlligatorOP
I've tried your example and confirm it shows correct not-found.tsx. After investigation, I discovered the following:
- if server action is passed as form action , notFound is handled automatically on client
- if server action is passed to any other handler (e.g. onClick or useEffect), notFound is NOT handled automatically

Modified example to demo it:
// page.tsx
export default function Page() {
  return (
    <Button
      action={async () => {
        "use server";
        notFound();
      }}
    />
  );
}

// button.tsx
"use client";

export default function Button({ action }) {
  return <button onClick={() => action()}>Click me</button>;
}
Chinese AlligatorOP
Created an issue in Next.js repo https://github.com/vercel/next.js/issues/69480
@Chinese Alligator Created an issue in Next.js repo https://github.com/vercel/next.js/issues/69480
good catch, yeah i think this is a bug.

worth to mention though that redirect still works, despite it working in more or less the same mechanism as notFound. so you can perhaps use redirect("/404") as a workaround at the moment.
Chinese AlligatorOP
I've digged more and found another workaround, inspired by this SO discussion: https://stackoverflow.com/questions/67889383/react-error-boundaries-with-useeffect-and-async-function-what-im-missing

By React docs, errors thrown in async code are not handled by error boundaries. That's why Next can't show appropriate not-found.txs (and error.tsx as well). Following the suggestion from SO, I've made it working by re-throwing the error during render:

// buttons.tsx
"use client";

import { useState } from "react";

export default function Button({ action }) {
  const [error, setError] = useState<Error | null>(null);

  // Re-throw error to be caught by error.tsx / not-found.tsx
  if (error) throw error;

  return (
    <button onClick={() => action().catch(setError)}>
      Button with server action
    </button>
  );
}
Answer
Chinese AlligatorOP
I want to add, that everything was in React docs:
https://react.dev/reference/rsc/use-server#calling-a-server-action-outside-of-form

When using a Server Action outside of a form, call the Server Action in a Transition, which allows you to display a loading indicator, show optimistic state updates, and handle unexpected errors. Forms will automatically wrap Server Actions in transitions.

Wrapping server action call into startTransition allows to omit error handling at all. Here is the final code:
// button.tsx
"use client";

import { useTransition } from "react";

export default function Button({ action }) {
  const [isPending, startTransition] = useTransition();

  const onClick = () => {
    startTransition(async () => {
      await action();
    });
  };

  return <button onClick={onClick}>Button with server action</button>;
}