
Safe Action Validation Errors
- 1k installs
- Updated July 18, 2026
- next-safe-action/skills
safe-action-validation-errors is a Claude Code skill that teaches developers to return structured, type-safe validation errors from Next.js server actions mapped to Zod schemas and React Hook Form fields.
About
safe-action-validation-errors is a Next.js server-action skill from next-safe-action/skills that shows how to use returnValidationErrors to emit field-level and form-level errors from Z.js actions with correct TypeScript inference. The workflow starts by passing the Zod input schema as the first argument so error shapes stay aligned with parsedInput types, then returning validation objects that React Hook Form can bind directly to inputs. Developers reach for safe-action-validation-errors when login, signup, or settings forms need server-side checks—duplicate email, weak password, business rules—without throwing exceptions or losing type safety. The skill assumes action files use the "use server" directive and actionClient.inputSchema patterns from next-safe-action.
- Returns field-level validation errors matching your Zod schema shape
- Supports multiple error messages per field
- Enables combined form-level and field-level error messages
- Works with nested object schemas for complex forms
- Provides full TypeScript inference from your input schema
Safe Action Validation Errors by the numbers
- 1,020 all-time installs (skills.sh)
- +25 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #391 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/next-safe-action/skills --skill safe-action-validation-errorsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1k |
|---|---|
| Security audit | 3 / 3 scanners passed |
| Last updated | July 18, 2026 |
| Repository | next-safe-action/skills ↗ |
How do Next.js server actions return typed form errors?
Return structured, type-safe validation errors from Next.js server actions that map directly to React Hook Form and Zod schemas.
Who is it for?
Next.js developers building server actions with Zod and React Hook Form who need structured, field-level error responses.
Skip if: Projects using REST APIs, tRPC, or client-only validation without next-safe-action server actions.
When should I use this skill?
User implements Next.js server actions with Zod and needs field-level validation errors for React Hook Form.
What you get
Type-safe validation error objects bound to React Hook Form fields and Zod schema keys
- Server action handlers with returnValidationErrors
- Field-mapped validation error responses
Files
next-safe-action Validation Errors
Two Sources of Validation Errors
1. Schema validation — automatic when input doesn't match .inputSchema() 2. 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:
// For schema: z.object({ email: z.string().email(), address: z.object({ city: z.string() }) })
{
_errors: ["Form-level error"], // root errors
email: { _errors: ["Invalid email address"] }, // field errors
address: {
_errors: ["Address section error"],
city: { _errors: ["City is required"] }, // nested field errors
},
}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 }) => {
// Check business rules after schema validation passes
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"] },
});
}
// Both checks passed — create the user
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
- Custom validation errors and returnValidationErrors patterns
- Formatted vs flattened shapes, per-action override
Displaying Validation Errors
// Formatted shape (default)
{result.validationErrors?.email?._errors?.map((error) => (
<p key={error} className="text-red-500">{error}</p>
))}
// Root-level errors
{result.validationErrors?._errors?.map((error) => (
<p key={error} className="text-red-500">{error}</p>
))}// Flattened shape
{result.validationErrors?.fieldErrors?.email?.map((error) => (
<p key={error} className="text-red-500">{error}</p>
))}
// Form-level errors (flattened)
{result.validationErrors?.formErrors?.map((error) => (
<p key={error} className="text-red-500">{error}</p>
))}Custom Validation Errors
Note: Action files require a "use server" directive — omitted from examples below for brevity.returnValidationErrors
import { returnValidationErrors } from "next-safe-action";The first argument must be the schema (for type inference). The second argument is the validation errors object matching the schema shape.
Field-Level Errors
const schema = z.object({
email: z.string().email(),
password: z.string().min(8),
});
export const login = actionClient
.inputSchema(schema)
.action(async ({ parsedInput }) => {
const user = await db.user.findByEmail(parsedInput.email);
if (!user) {
returnValidationErrors(schema, {
email: { _errors: ["No account found with this email"] },
});
}
const validPassword = await verifyPassword(parsedInput.password, user.passwordHash);
if (!validPassword) {
returnValidationErrors(schema, {
password: { _errors: ["Incorrect password"] },
});
}
return { userId: user.id };
});Multiple Errors Per Field
returnValidationErrors(schema, {
password: {
_errors: [
"Must contain at least one uppercase letter",
"Must contain at least one number",
],
},
});Form-Level + Field-Level Errors
returnValidationErrors(schema, {
_errors: ["Unable to complete registration at this time"],
email: { _errors: ["This email domain is not allowed"] },
});Nested Object Errors
const schema = z.object({
address: z.object({
street: z.string(),
city: z.string(),
zip: z.string(),
}),
});
returnValidationErrors(schema, {
address: {
zip: { _errors: ["Invalid ZIP code for the selected state"] },
},
});Error Classes
Note:ActionServerValidationErroris used internally byreturnValidationErrors()— it is not exported from the package and should not be imported directly.
ActionValidationError
Thrown when throwValidationErrors is enabled and input validation fails. Catch this in try/catch:
import { ActionValidationError } from "next-safe-action";
try {
const result = await myAction({ invalidInput: true });
} catch (e) {
if (e instanceof ActionValidationError) {
console.log(e.validationErrors); // The validation errors object
console.log(e.message); // Error message (default or overridden)
}
}ActionBindArgsValidationError
Thrown when bind args fail validation:
import { ActionBindArgsValidationError } from "next-safe-action";ActionMetadataValidationError
Thrown when metadata doesn't match defineMetadataSchema:
import { ActionMetadataValidationError } from "next-safe-action";ActionOutputDataValidationError
Thrown when the action's return value doesn't match outputSchema. Has a .validationErrors property with the validation failure details.
import { ActionOutputDataValidationError } from "next-safe-action";Validation Error Shapes
Note: Action files require a "use server" directive — omitted from examples below for brevity.Formatted (Default)
Nested structure mirroring the schema, with _errors arrays at each level:
// Schema: z.object({ email: z.string().email(), name: z.string().min(2) })
// Formatted errors:
{
_errors: [], // root-level errors
email: { _errors: ["Invalid email"] },
name: { _errors: ["Too short"] },
}Access: result.validationErrors?.email?._errors?.[0]
Flattened
Flat structure with formErrors (root) and fieldErrors (one level deep):
// Same schema, flattened errors:
{
formErrors: [], // root-level errors
fieldErrors: {
email: ["Invalid email"],
name: ["Too short"],
},
}Access: result.validationErrors?.fieldErrors?.email?.[0]
Note: Flattened mode only processes one level deep. Nested object field errors are not included.
Setting the Default Shape
Client-Level Default
import { createSafeActionClient } from "next-safe-action";
export const actionClient = createSafeActionClient({
defaultValidationErrorsShape: "flattened", // "formatted" | "flattened"
});All actions created from this client will use the flattened shape by default.
Per-Action Override
Override the shape for a specific action using handleValidationErrorsShape in .inputSchema():
import { flattenValidationErrors, formatValidationErrors } from "next-safe-action";
// Client uses "formatted" by default, but this action uses "flattened"
export const myAction = actionClient
.inputSchema(
z.object({ email: z.string().email() }),
{
handleValidationErrorsShape: async (ve) => flattenValidationErrors(ve),
}
)
.action(async ({ parsedInput }) => {
// ...
});Custom Shape
Return any shape you want from handleValidationErrorsShape:
export const myAction = actionClient
.inputSchema(
z.object({ email: z.string().email(), name: z.string() }),
{
handleValidationErrorsShape: async (ve) => {
// Custom: just a flat record of field → first error
const errors: Record<string, string> = {};
for (const [key, value] of Object.entries(ve)) {
if (key !== "_errors" && value?._errors?.[0]) {
errors[key] = value._errors[0];
}
}
return errors;
},
}
)
.action(async ({ parsedInput }) => { /* ... */ });
// result.validationErrors: { email?: string; name?: string }Utility Functions
formatValidationErrors(ve)
Identity function — returns errors as-is (formatted shape). Useful when you want to be explicit:
import { formatValidationErrors } from "next-safe-action";
handleValidationErrorsShape: async (ve) => formatValidationErrors(ve),flattenValidationErrors(ve)
Converts formatted → flattened shape:
import { flattenValidationErrors } from "next-safe-action";
handleValidationErrorsShape: async (ve) => flattenValidationErrors(ve),handleValidationErrorsShape Receives Context
The function receives a second utils argument with full context:
handleValidationErrorsShape: async (ve, { clientInput, bindArgsClientInputs, metadata, ctx }) => {
// Log the validation failure with context
logger.warn("Validation failed", {
action: metadata.actionName,
userId: ctx.userId,
errors: ve,
});
return flattenValidationErrors(ve);
},throwValidationErrors
When enabled, validation errors throw ActionValidationError instead of being returned in result.validationErrors. The thrown error contains the shaped validation errors (after handleValidationErrorsShape runs).
// Enable globally
const actionClient = createSafeActionClient({
throwValidationErrors: true,
});
// Or per-action
export const myAction = actionClient
.inputSchema(schema)
.action(serverCodeFn, {
throwValidationErrors: true,
});
// With custom error message
export const myAction = actionClient
.inputSchema(schema)
.action(serverCodeFn, {
throwValidationErrors: {
overrideErrorMessage: async (validationErrors) =>
`Validation failed: ${JSON.stringify(validationErrors)}`,
},
});Related skills
How it compares
Pick this over generic Zod error-handling guides when the stack already uses next-safe-action actionClient and React Hook Form together.
FAQ
How does returnValidationErrors work in next-safe-action?
safe-action-validation-errors documents returnValidationErrors, which takes the Zod input schema first for type inference and a second argument matching the schema shape. Server actions return these objects instead of throwing, letting React Hook Form display field-level messages
Can Next.js server actions return field-specific form errors?
safe-action-validation-errors shows field-level and form-level error objects from next-safe-action actions. Errors align with Zod schema keys so React Hook Form can bind them directly to inputs without manual mapping.