| name | safe-action-validation-errors |
| description | Use when working with validation errors -- returnValidationErrors, formatted vs flattened shapes, custom validation error shapes, throwValidationErrors, or displaying field-level and form-level errors |
next-safe-action Validation Errors
Two Sources of Validation Errors
- Schema validation — automatic when input doesn't match
.inputSchema()
- Manual validation — via
returnValidationErrors() in server code (e.g., "email already taken")
Both produce the same error structure on the client.
Default Error Shape (Formatted)
Mirrors the schema structure with _errors arrays at each level:
{
_errors: ["Form-level error"],
email: { _errors: ["Invalid email address"] },
address: {
_errors: ["Address section error"],
city: { _errors: ["City is required"] },
},
}
returnValidationErrors
Throws a ActionServerValidationError that the framework catches and returns as result.validationErrors. It never returns — it always throws.
"use server";
import { z } from "zod";
import { returnValidationErrors } from "next-safe-action";
import { actionClient } from "@/lib/safe-action";
const registerSchema = z.object({
email: z.string().email(),
username: z.string().min(3),
});
export const register = actionClient.inputSchema(registerSchema).action(async ({ parsedInput }) => {
const existingUser = await db.user.findByEmail(parsedInput.email);
if (existingUser) {
returnValidationErrors(registerSchema, {
email: { _errors: ["This email is already registered"] },
});
}
const existingUsername = await db.user.findByUsername(parsedInput.username);
if (existingUsername) {
returnValidationErrors(registerSchema, {
username: { _errors: ["This username is taken"] },
});
}
const user = await db.user.create(parsedInput);
return { id: user.id };
});
Root-Level Errors
Use _errors at the top level for form-wide errors:
returnValidationErrors(schema, {
_errors: ["You can only create 5 posts per day"],
});
Supporting Docs
Displaying Validation Errors
{
result.validationErrors?.email?._errors?.map((error) => (
<p key={error} className="text-red-500">
{error}
</p>
));
}
{
result.validationErrors?._errors?.map((error) => (
<p key={error} className="text-red-500">
{error}
</p>
));
}
{
result.validationErrors?.fieldErrors?.email?.map((error) => (
<p key={error} className="text-red-500">
{error}
</p>
));
}
{
result.validationErrors?.formErrors?.map((error) => (
<p key={error} className="text-red-500">
{error}
</p>
));
}