safe-action-validation-errors

Use when working with validation errors -- returnValidationErrors, formatted vs flattened shapes, custom validation error shapes, throwValidationErrors, or…

INSTALLATION
npx skills add https://github.com/next-safe-action/skills --skill safe-action-validation-errors
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

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:

// 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

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>

))}
BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card