I'm trying to do error handling in Next.js. I'm using a server action that is passed to a form and invoked when the form is submitted, like this::
Form Component
"use client";
import PasswordInput from "@components/PasswordInput";
import ProvidersLogin from "@components/ProvidersLogin";
import { Button, Divider, Input, Link } from "@nextui-org/react";
import loginCredentials from "@utils/loginCredentials";
import ErrorToast from "@components/ErrorToast";
import { useFormState } from "react-dom";
import { useEffect } from "react";
const initialState = {
message: "",
};
export default function Login() {
const [error, formAction, isPending] = useFormState(
loginCredentials,
initialState
);
useEffect(() => {
console.log("error", error);
}, [error]);
return (
<div className="flex flex-col gap-4 items-center justify-center w-full h-full min-h-screen">
<form action={formAction} className="flex flex-col gap-4 min-w-96">
<h1 className="font-black text-4xl">Login</h1>
<p className="text-default-500 text-lg">
Let{"'"}s make it even better
</p>
<ProvidersLogin />
{/* Commented till error handling is fixed, as currently it does nothing */}
<Divider />
<Input
type="email"
name="email"
label="Email"
labelPlacement="outside"
placeholder="Enter your email"
size="lg"
// biome-ignore lint/a11y/noPositiveTabindex: <explanation>
tabIndex={1}
required
isDisabled={isPending}
// description=""
/>
<div className="relative flex flex-col w-full">
<Link
className="absolute -top-1 tap-highlight-transparent outline-none text-medium text-primary no-underline hover:opacity-80 active:opacity-disabled transition-opacity self-end"
href="/forgotpassword"
>
Forgot Password?
</Link>
<PasswordInput
name="password"
label="Password"
placeholder="Enter your password"
required
// biome-ignore lint/a11y/noPositiveTabindex: <explanation>
tabIndex={2}
isDisabled={isPending}
/>
</div>
<div className="flex flex-row gap-1.5 justify-center items-center w-full -mt-2 -mb-1">
Don{"'"}t have an account?
<Link href="/signup">Sign Up</Link>
</div>
<p>{error?.message}</p>
<div className="flex flex-row gap-4 items-center justify-between">
<Button
as={Link}
href="/"
color="danger"
variant="light"
isDisabled={isPending}
>
Cancel
</Button>
<Button type="submit" color="primary" isLoading={isPending}>
Login
</Button>
</div>
</form>
</div>
);
}
Server Action
"use server";
import { signIn } from "@auth";
import { redirect, RedirectType } from "next/navigation";
export default async function loginCredentials(
prevState: { message: string },
data: FormData
) {
try {
const email = data.get("email")?.toString();
const password = data.get("password")?.toString();
if (!email || !password) {
throw new Error("Please fill in all fields");
}
await signIn("credentials", {
email,
password,
redirect: false,
// callbackUrl: "/dashboard",
});
} catch (error) {
// @ts-ignore
return { message: error.cause.err.message };
}
redirect("/dashboard", RedirectType.replace);
}
Note: I know I'm using error.cause.err.message
as in next-auth 5, when an error is thrown by a CredentialsSignIn error in the Credentials provider authorize function (If the signIn function is called on the server-side), the message from the CredentialsSignIn is stored in error.cause.err.message
.
the issue is when I tried to implement this the server action didn't return the response object, i.e. the state
remains undefined
I even tried an implementation of this without the useFormState
:
Form Component
"use client";
import PasswordInput from "@components/PasswordInput";
import ProvidersLogin from "@components/ProvidersLogin";
import { Button, Divider, Input, Link } from "@nextui-org/react";
import loginCredentials from "@utils/loginCredentials";
import ErrorToast from "@components/ErrorToast";
import { useFormState } from "react-dom";
import { useEffect } from "react";
const initialState = {
message: "",
};
export default function Login() {
const [error, setError] = useState<string>()
const [isPending, setIsPending] = useState(false)
useEffect(() => {
console.log("error", error);
}, [error]);
return (
<div className="flex flex-col gap-4 items-center justify-center w-full h-full min-h-screen">
<form action={async (data) => {
setIsPending(true)
const res = await loginCredentials({message: ""}, data)
if (res?.message) {
setError(res.message)
}
}} className="flex flex-col gap-4 min-w-96">
<h1 className="font-black text-4xl">Login</h1>
<p className="text-default-500 text-lg">
Let{"'"}s make it even better
</p>
<ProvidersLogin />
{/* Commented till error handling is fixed, as currently it does nothing */}
<Divider />
<Input
type="email"
name="email"
label="Email"
labelPlacement="outside"
placeholder="Enter your email"
size="lg"
// biome-ignore lint/a11y/noPositiveTabindex: <explanation>
tabIndex={1}
required
isDisabled={isPending}
// description=""
/>
<div className="relative flex flex-col w-full">
<Link
className="absolute -top-1 tap-highlight-transparent outline-none text-medium text-primary no-underline hover:opacity-80 active:opacity-disabled transition-opacity self-end"
href="/forgotpassword"
>
Forgot Password?
</Link>
<PasswordInput
name="password"
label="Password"
placeholder="Enter your password"
required
// biome-ignore lint/a11y/noPositiveTabindex: <explanation>
tabIndex={2}
isDisabled={isPending}
/>
</div>
<div className="flex flex-row gap-1.5 justify-center items-center w-full -mt-2 -mb-1">
Don{"'"}t have an account?
<Link href="/signup">Sign Up</Link>
</div>
<p>{error}</p>
<div className="flex flex-row gap-4 items-center justify-between">
<Button
as={Link}
href="/"
color="danger"
variant="light"
isDisabled={isPending}
>
Cancel
</Button>
<Button type="submit" color="primary" isLoading={isPending}>
Login
</Button>
</div>
</form>
</div>
);
}
yet the server action returns undefined
, I also checked the the server action is actually invoked and it is invoked.
Please, someone provide a solution to this
Note: I know that useFormState
is gonna be replaced by useActionState
, I tried it and it's still returning undefined
I finally figured it out.
the issue was that I was trying to modify the headers in the middleware
once I stopped passing the headers to NextResponse.next()
the issue was fixed.
I incorrectly added all searchParams to the response headers. Didn't realize they weren't just strings, some were functions. This caused invalid headers, leading to data loss. Console logging shows many headers disappearing after modification. I suspect server actions rely on specific headers to communicate with client components, and their absence prevented data transfer. You may need to investigate further.
You can still add headers to your response, just make sure they are valid.