Next.js 14中Next Auth凭证登录结合Server Actions的错误处理问题
解决Next.js 14 Server Action登录错误捕获问题
你的问题核心在于Server Action里的错误都是通过返回对象的方式传递,而非抛出异常,所以客户端的try/catch无法捕获这些错误,全部进入了try块。要让错误被catch捕获,需要把Server Action中的错误改为抛出异常,而非返回错误对象。
步骤1:修改Server Action,抛出错误而非返回错误对象
改造/actions/login.ts,将所有验证错误、凭证错误改为抛出异常,同时为字段验证错误自定义一个错误类型,方便客户端区分处理:
"use server"; import { signIn } from "@/auth"; import { DEFAULT_REDIRECT } from "@/routes"; import { LoginFormSchema, LoginFormSchemaType } from "@/schemas"; import { getUserByEmail } from "@/utils/db/data/user"; import bcrypt from "bcryptjs"; import { toast } from "sonner"; // 自定义字段验证错误类,携带字段错误信息 class FieldValidationError extends Error { fieldErrors: Record<string, string[]>; constructor(fieldErrors: Record<string, string[]>) { super("请验证您的输入数据"); this.name = "FieldValidationError"; this.fieldErrors = fieldErrors; } } export const login = async (values: LoginFormSchemaType) => { const validatedFields = LoginFormSchema.safeParse(values); if (!validatedFields.success) { // 抛出字段验证错误,替代返回对象 throw new FieldValidationError(validatedFields.error.flatten().fieldErrors); } const { email, password } = validatedFields.data; const existingUser = await getUserByEmail(email); if (!existingUser || !existingUser.email || !existingUser.password) { // 抛出凭证错误 throw new Error("邮箱或密码无效"); } const passwordsMatch = await bcrypt.compare(password, existingUser.password); if (!passwordsMatch) { // 抛出凭证错误 throw new Error("邮箱或密码无效"); } try { await signIn("credentials", { email, password, redirectTo: DEFAULT_REDIRECT, }); toast(`Welcome back ${existingUser.name} `); return { success: true, message: "登录成功" }; } catch (error: any) { // 重新抛出Auth.js的错误 throw new Error(error.message || "登录失败"); } };
步骤2:修改客户端表单,捕获并处理不同类型的错误
在/app/(auth)/login/form.tsx中,修改onSubmit方法,区分捕获到的错误类型,分别处理字段错误和通用错误:
// ----------------------------- import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { LoginFormSchema, LoginFormSchemaType } from "@/schemas"; import { login } from "@/actions/login"; import { useState } from "react"; import { Form, FormField, FormItem, FormLabel, FormControl, Input, FormMessage } from "@/components/ui/form"; import { Button } from "@/components/ui/button"; // ----------------------------- const loginForm = useForm<LoginFormSchemaType>({ resolver: zodResolver(LoginFormSchema), defaultValues: { email: "", password: "", }, }); // ------------------------ const [formMessage, setFormMessage] = useState<{ message: string; type: "error" | "success"; } | null>(null); const [loadingToLogin, setLoadingToLogin] = useState(false); const onSubmit = async (values: LoginFormSchemaType) => { setFormMessage(null); setLoadingToLogin(true); try { const response = await login(values); // 登录成功时处理提示 setFormMessage({ message: response.message, type: "success", }); } catch (error: any) { // 区分错误类型,针对性处理 if (error.name === "FieldValidationError") { // 字段验证错误:绑定到对应表单字段显示 Object.entries(error.fieldErrors).forEach(([field, messages]) => { loginForm.setError(field as keyof LoginFormSchemaType, { message: messages.join(", "), }); }); } else { // 通用错误:显示在表单顶部 setFormMessage({ message: error.message, type: "error", }); } } finally { // 无论成功失败,重置加载状态 setLoadingToLogin(false); } }; return ( <Form {...loginForm}> <form onSubmit={loginForm.handleSubmit(onSubmit)} className="grid gap-4"> {/* 表单全局提示 */} {formMessage && ( <div className={`text-sm ${formMessage.type === "error" ? "text-red-500" : "text-green-500"}`}> {formMessage.message} </div> )} <FormField control={loginForm.control} name="email" render={({ field }) => ( <FormItem> <FormLabel>邮箱</FormLabel> <FormControl> <Input {...field} autoComplete="email" disabled={loadingToLogin} /> </FormControl> <FormMessage /> </FormItem> )} /> <FormField control={loginForm.control} name="password" render={({ field }) => ( <FormItem> <FormLabel>密码</FormLabel> <FormControl> <Input type="password" {...field} autoComplete="current-password" disabled={loadingToLogin} /> </FormControl> <FormMessage /> </FormItem> )} /> <Button type="submit" disabled={loadingToLogin} className="w-full"> {loadingToLogin ? "登录中..." : "登录"} </Button> </form> </Form> );
关键说明
- 自定义错误类:通过
FieldValidationError区分字段验证错误和其他通用错误,让客户端可以针对性处理——字段错误绑定到对应表单字段显示,通用错误显示在表单顶部。 - 抛出异常替代返回错误对象:Server Action中所有错误场景都改为
throw,确保客户端的try/catch能捕获到错误。 - finally块处理加载状态:保证无论登录成功还是失败,加载状态都会被重置,避免按钮一直处于禁用状态。
内容的提问来源于stack exchange,提问作者barhoom
相关产品推荐
相关产品推荐

