Error handling for Server Actions #49426
-
Hey 👋 How to handle errors in server actions and use those errors to show alerts/toast in UI? I looked into the examples provided in the docs, and they are directly throwing the error. But how can we use that error in UI? |
Beta Was this translation helpful? Give feedback.
Replies: 20 comments 71 replies
-
Similar to fail in Sveltekit and badRequest in Remix |
Beta Was this translation helpful? Give feedback.
-
Edit (15th September 2023) With #55399, this has now landed in Next.js with It's still pretty rough around the edges (as is to be expected), but this is so exciting! Here's a good example of usage: https://stackblitz.com/edit/vitejs-vite-qmhsah 🎉🎉 |
Beta Was this translation helpful? Give feedback.
-
@sam3d sorry, I'm still confused though, how we present the error to the user? we don't have |
Beta Was this translation helpful? Give feedback.
-
Would be neat if const { pending, response, error } = useFormStatus(action) where You could use it in a generic import { useEffect } from 'react'
import { useFormStatus } from 'react-dom'
import { toast } from 'react-hot-toast'
export function Form({ loading, submit = 'Send', action, children, ...props }) {
const { pending, response, error } = useFormStatus(action)
useEffect(() => {
if (pending) toast.loading(loading)
if (error) toast.error(error.message)
if (response) toast.success(response.message)
}, [pending, response, error, loading])
return (
<form {...props} action={action}>
{children}
<button aria-busy={pending}>{submit}</button>
</form>
)
} |
Beta Was this translation helpful? Give feedback.
-
Until react has a built-in hook you can use this one. I just created it so please let me know what you think! const [run, { error, loading }] = useActionState(action); Note that it is only for client side. |
Beta Was this translation helpful? Give feedback.
-
IMHO server actions feel pretty incomplete when there is no built-in (server-supported) way to "return" a response for:
When there is no other visual confirmation of your action being dispatched successfully, this is really important I think. I could think of an API which works for server and client components alike, which uses the
The |
Beta Was this translation helpful? Give feedback.
-
I have also faced this issue when trying to build a login form. When the user submits the data, I need to inform them about the server response if the "email" or "password" do not match. I spent some time tackling this problem, and here is the solution. this solution also works when the javascript is disabled in the browser. I created a class that manipulates the server response like this: class AtomicState {
constructor(public message: string | undefined = undefined) {}
setMessage(message: string) {
this.message = message;
}
getMessage() {
const message = this.message;
this.message = undefined;
return message;
}
} This class is responsible for setting and getting response messages. The I can use this class as follows: const state = new AtomicState(); and this the my const login = async (formData: FormData) => {
"use server";
const email = formData.get("email");
const password = formData.get("password");
const res = await fetch("http://localhost:4000/users/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password }),
});
const json = await res.json();
if (res.ok) {
state.setMessage("Successfully logged in");
redirect("/chat");
} else {
state.setMessage(json.error);
revalidatePath("/login");
}
};
You can access the data from the state and render it in the UI like this: ...
<h2>{state.getMessage()}</h2>
... Here is the whole file: import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
class AtomicState {
constructor(public message: string | undefined = undefined) {}
setMessage(message: string) {
this.message = message;
}
getMessage() {
const message = this.message;
this.message = undefined;
return message;
}
}
const state = new AtomicState();
const login = async (formData: FormData) => {
"use server";
const email = formData.get("email");
const password = formData.get("password");
const res = await fetch("http://localhost:4000/users/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password }),
});
const json = await res.json();
console.log(json);
if (res.ok) {
state.setMessage("Successfully logged in");
redirect("/chat");
} else {
state.setMessage(json.error);
revalidatePath("/login");
}
};
export default function Login() {
return (
<main className="min-h-screen bg-gray-100 grid place-content-center">
<h1 className="text-center text-5xl mb-8">Login</h1>
<h2>{state.getMessage()}</h2>
<form
className="flex flex-col gap-y-4 bg-gray-700 p-14 rounded-xl"
action={login}
>
<div className="form-control">
<label className="label" htmlFor="email">
Email
</label>
<input
className="input"
id="email"
type="email"
placeholder="Email"
name="email"
/>
</div>
<div className="form-control">
<label className="label" htmlFor="password">
Password
</label>
<input
className="input"
type="password"
id="password"
placeholder="Password"
name="password"
/>
</div>
<button className="btn" type="submit">
Login
</button>
</form>
</main>
);
} |
Beta Was this translation helpful? Give feedback.
-
Following up @MustafaWael solution. For Server -> Client, create an object (not a class): let signInState: SignInState = {
confirmation: "",
error: null,
}; Then I pass it to the client component: <form action={passwordLessSignin}>
<EmailSignIn state={signInState} /> {/* Client Component */ }
</form> The email sign in uses the state as usual, but I update it in the server action: async function passwordLessSignin(formData: FormData) {
"use server";
const { res, serverError } = /* ... */;
signInState = {
confirmation: "Sent to email", /* res */
error: null /* serverError */
}
revalidatePath("/"); // makes the new state show
} This way it works even with the server-client gap. |
Beta Was this translation helpful? Give feedback.
-
You can also call |
Beta Was this translation helpful? Give feedback.
-
To handle form errors in my app, i used HTTP sessions. But i had to implement them from scratch and use REDIS for storing them. I don't really recommend anyone to go that route for this simple case and there will be soon a primitive from react to handle that, but if you want to know more, you can check out this repo : https://github.com/Fredkiss3/next-custom-session |
Beta Was this translation helpful? Give feedback.
-
@ma13gagan in I created a demo for this here https://github.com/Fredkiss3/form-errors-pe // form.ts
"use client";
import {
experimental_useFormState as useFormState,
experimental_useFormStatus as useFormStatus,
} from "react-dom";
import { action } from "./_action";
export default function MyForm() {
const [state, dispatch] = useFormState(action, {
message: null,
type: undefined,
});
return (
<main>
<h2 >useFormState demo</h2>
<h1>Disable JavaScript to test with Progressive Enhancement</h1>
{state?.type === "success" && <Alert>{state.message}</Alert>}
<form action={dispatch}>
<label htmlFor="name">Your Name</label>
<input
id="name"
name="name"
placeholder="John Doe"
aria-describedby={`name-error`}
className={`border rounded-md p-2 ${
state?.type === "error" && state?.errors?.name
? "accent-red-400"
: ""
}`}
/>
{state?.type === "error" && state?.errors?.name && (
<span id="name-error" className="text-red-400">
{state.errors.name.join(",")}
</span>
)}
<label htmlFor="message">Your Message</label>
<textarea
id="message"
style={{
width: "100%",
}}
name="message"
placeholder="I love cheese"
aria-describedby={`message-error`}
className={`border rounded-md p-2 ${
state?.type === "error" && state?.errors?.message
? "accent-red-400"
: ""
}`}
/>
{state?.type === "error" && state?.errors?.message && (
<span id="message-error" className="text-red-400">
{state.errors.message.join(",")}
</span>
)}
<SubmitButton />
</form>
</main>
);
}
function SubmitButton() {
const status = useFormStatus();
return (
<button
aria-disabled={status.pending}
onClick={(e) => {
// prevent multiple submits
if (status.pending) e.preventDefault();
}}
className={`rounded-md text-white px-4 py-2 ${
status.pending ? "bg-blue-300" : "bg-blue-400"
}`}
>
{status.pending ? "Submiting..." : "Submit"}
</button>
);
} // _action.ts
"use server";
import { z } from "zod";
type ActionResult =
| {
type: "success";
message: string;
}
| {
type: "error";
errors: Record<string, string[] | undefined>;
}
| { type: undefined; message: null };
const formSchema = z.object({
name: z.string().min(1),
message: z.string().min(3),
});
export async function action(prevState: ActionResult, payload: FormData) {
const result = formSchema.safeParse(Object.fromEntries(payload.entries()));
if (result.success) {
return {
type: "success" as const,
message: `Name=${result.data.name}; Value=${result.data.message}`,
};
} satisfies ActionResult;
return {
type: "error" as const,
errors: result.error.flatten().fieldErrors,
} satisfies ActionResult;
} |
Beta Was this translation helpful? Give feedback.
-
Beta Was this translation helpful? Give feedback.
-
It seems to me that it is not very correct to return a response to each action. If we have a modular system and different developers work with each module, then we may have a different response structure in the actions of different modules. In this regard, there are suggestions:
It seems to me that this will be the right decision. If we use errors in the form of response, then when we change the response in the api, we will have to change all the actions, instead of replacing it in one place. There is something to think about. |
Beta Was this translation helpful? Give feedback.
-
To expand on this a bit: I would like to validate data both on the client & server. I am currently using |
Beta Was this translation helpful? Give feedback.
-
yooooo, just built a whole app throwing errors in server actions expecting them to be caught in the client, only to run prod build and see it not work. Are you kidding me? |
Beta Was this translation helpful? Give feedback.
-
I hope this will help someone. I had the situation where i would like to handle API responses like HTTP 403, 401, 404 etc. and, 200 OK. Returning the error via throw new Error(errorMessage) does not work directly but we can bypass this. My server action puts the response in a function that returns an object. This object can either contain succes data or error data. That way i have an object with all the data i need (respone status, status text, message, errors and a boolean where i can check if it was an error or not). I than just make choices based on this object.
Now at the form i can check if there are errors and do whatever i like. Note: I can also return the object when it has success so i can than put that message in a toaster at my form but i was not able to do that when i use a redirect at the server action (this only returns true ofcourrse), so i ended up doings it this way. server-action function (actions.ts)
The function that creates the response object
My form that runs the server action (this is a client component)
Have a nice day! |
Beta Was this translation helpful? Give feedback.
-
Right now, I use Next-safe-action handle server action. I help me a lot for response status like HTTP. |
Beta Was this translation helpful? Give feedback.
-
So now that
Caused by: import { useActionState } from "react"; Do we have to wait for support from Next? |
Beta Was this translation helpful? Give feedback.
-
What I've been doing is using transitions and For example: const [isUpdatePending, startUpdateTransition] = React.useTransition();
...
startUpdateTransition(() => {
toast.promise(
rescheduleSessionAction({
accountId: props.session.account_id,
workoutId: props.session.workout_id,
operation: "reschedule-to-today",
toDate: new Date(),
}),
{
loading: "Updating training calendar...",
success:
"Training calendar updated successfully",
error: "Failed to update training calendar",
},
);
}); Handles all three states
It's been a super clean way of doing things :) |
Beta Was this translation helpful? Give feedback.
-
Hey everyone, I just updated our docs with guidance on handling errors with Server Actions! https://rc.nextjs.org/docs/app/building-your-application/routing/error-handling |
Beta Was this translation helpful? Give feedback.
Hey everyone, I just updated our docs with guidance on handling errors with Server Actions!
https://rc.nextjs.org/docs/app/building-your-application/routing/error-handling