Claude Code Plugins

Community-maintained marketplace

Feedback

customizing-errors

@djankies/claude-configs
0
0

Handle Zod validation errors with unified error API, custom messages, error formatting, and user-friendly display

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name customizing-errors
description Handle Zod validation errors with unified error API, custom messages, error formatting, and user-friendly display

Handling Zod Errors

Purpose

Comprehensive guide to error handling in Zod v4, covering the unified error API, custom error messages, error formatting, and integration with UI frameworks.

Unified Error API

Overview

Zod v4 unified all error customization under a single error parameter, replacing multiple deprecated parameters.

v3 (deprecated):

z.string({ message: "Required" });
z.string({ invalid_type_error: "Must be string" });
z.string({ required_error: "Field required" });
z.object({}, { errorMap: customMap });

v4 (unified):

z.string({ error: "Required" });
z.string({ error: "Must be string" });
z.string({ error: "Field required" });
z.object({}, { error: customMap });

Error Parameter Types

String messages:

z.string({ error: "This field is required" });
z.email({ error: "Please enter a valid email address" });
z.number({ error: "Must be a number" }).min(0, {
  error: "Must be positive"
});

Error map functions:

const customErrorMap: ZodErrorMap = (issue, ctx) => {
  if (issue.code === z.ZodIssueCode.invalid_type) {
    return { message: `Expected ${issue.expected}, got ${issue.received}` };
  }
  return { message: ctx.defaultError };
};

z.string({ error: customErrorMap });

SafeParse Pattern

Best Practice

Always use safeParse instead of parse wrapped in try/catch:

Anti-pattern:

try {
  const data = schema.parse(input);
  return data;
} catch (error) {
  console.error(error);
  return null;
}

Best practice:

const result = schema.safeParse(input);
if (!result.success) {
  console.error(result.error);
  return null;
}
return result.data;

Benefits:

  • No exception overhead (better performance)
  • Type-safe discriminated union
  • More readable control flow
  • Explicit error handling

Type Inference

const result = schema.safeParse(input);

if (result.success) {
  const data: z.infer<typeof schema> = result.data;
} else {
  const error: z.ZodError = result.error;
}

Error Formatting

Flatten Errors

Convert nested error structure to flat format:

const userSchema = z.object({
  email: z.email(),
  age: z.number().min(18)
});

const result = userSchema.safeParse({
  email: 'invalid',
  age: 10
});

if (!result.success) {
  const flattened = result.error.flatten();
  console.log(flattened.fieldErrors);
}

Output:

{
  email: ["Invalid email"],
  age: ["Number must be greater than or equal to 18"]
}

Format for Forms

React Hook Form integration:

const result = formSchema.safeParse(data);

if (!result.success) {
  const errors = result.error.flatten().fieldErrors;

  return {
    errors: {
      email: errors.email?.[0],
      password: errors.password?.[0]
    }
  };
}

Format for API Responses

const result = schema.safeParse(data);

if (!result.success) {
  return {
    success: false,
    errors: result.error.flatten().fieldErrors
  };
}

return {
  success: true,
  data: result.data
};

Custom Error Messages

Field-Level Errors

const userSchema = z.object({
  email: z.email({
    error: "Please enter a valid email address"
  }),
  password: z.string({
    error: "Password is required"
  }).min(8, {
    error: "Password must be at least 8 characters"
  }),
  age: z.number({
    error: "Age must be a number"
  }).min(18, {
    error: "You must be 18 or older"
  })
});

Refinement Errors

const passwordSchema = z.string().refine(
  (password) => /[A-Z]/.test(password),
  { error: "Password must contain at least one uppercase letter" }
).refine(
  (password) => /[0-9]/.test(password),
  { error: "Password must contain at least one number" }
);

Dynamic Error Messages

const rangeSchema = (min: number, max: number) =>
  z.number().refine(
    (val) => val >= min && val <= max,
    { error: `Value must be between ${min} and ${max}` }
  );

Error Maps

Global Error Map

import { z } from 'zod';

const customErrorMap: z.ZodErrorMap = (issue, ctx) => {
  if (issue.code === z.ZodIssueCode.invalid_type) {
    if (issue.expected === 'string') {
      return { message: "This field must be text" };
    }
  }

  if (issue.code === z.ZodIssueCode.too_small) {
    if (issue.type === 'string') {
      return { message: `Minimum ${issue.minimum} characters required` };
    }
  }

  return { message: ctx.defaultError };
};

z.setErrorMap(customErrorMap);

Schema-Specific Error Map

const userSchema = z.object({
  email: z.email(),
  age: z.number()
}, {
  error: (issue, ctx) => {
    if (issue.path[0] === 'email') {
      return { message: "Email address is invalid" };
    }
    return { message: ctx.defaultError };
  }
});

Error Code Reference

const errorMap: z.ZodErrorMap = (issue, ctx) => {
  switch (issue.code) {
    case z.ZodIssueCode.invalid_type:
      return { message: `Expected ${issue.expected}, got ${issue.received}` };

    case z.ZodIssueCode.invalid_string:
      return { message: "Invalid format" };

    case z.ZodIssueCode.too_small:
      return { message: `Minimum ${issue.minimum} required` };

    case z.ZodIssueCode.too_big:
      return { message: `Maximum ${issue.maximum} allowed` };

    case z.ZodIssueCode.invalid_enum_value:
      return { message: `Must be one of: ${issue.options.join(', ')}` };

    case z.ZodIssueCode.custom:
      return { message: issue.message ?? "Invalid value" };

    default:
      return { message: ctx.defaultError };
  }
};

Error Precedence

Zod v4 error precedence order (highest to lowest):

  1. Schema-level error parameter
  2. Parse-level error map
  3. Global error map (via z.setErrorMap())
  4. Locale error map
  5. Default Zod errors
z.setErrorMap(globalMap);

const schema = z.string({ error: schemaLevelMap });

const result = schema.safeParse(data, { errorMap: parseLevelMap });

Pretty Print Errors

Console Output

import { z } from 'zod';

const result = schema.safeParse(data);

if (!result.success) {
  console.error(z.prettifyError(result.error));
}

Format Issues

const result = schema.safeParse(data);

if (!result.success) {
  result.error.issues.forEach(issue => {
    console.log(`Field: ${issue.path.join('.')}`);
    console.log(`Error: ${issue.message}`);
    console.log(`Code: ${issue.code}`);
  });
}

Framework Integration

Zod integrates seamlessly with popular frameworks for error handling.

Quick patterns:

  • React Forms: Use safeParse + flatten().fieldErrors
  • React Hook Form: Use zodResolver for automatic integration
  • Next.js Server Actions: Return flattened errors to client
  • Express API: Return 400 status with error details

For framework-specific examples, see the integrating-zod-frameworks skill from the zod-4 plugin.

Best Practices

1. Always Use SafeParse

const result = schema.safeParse(data);  // ✅
try { schema.parse(data) }              // ❌

2. Provide User-Friendly Messages

z.email({ error: "Please enter a valid email address" })  // ✅
z.email()                                                  // ❌ Generic error

3. Flatten for Form Display

const errors = result.error.flatten().fieldErrors;  // ✅
const errors = result.error.issues;                 // ❌ Complex structure

4. Use Error Maps for Consistency

z.setErrorMap(customMap);  // ✅ Consistent across app

5. Test Error Paths

it('shows error for invalid email', () => {
  const result = schema.safeParse({ email: 'invalid' });
  expect(result.success).toBe(false);
  if (!result.success) {
    expect(result.error.issues[0].message).toBe("Invalid email");
  }
});

Migration from v3

Error Parameters

Before:

z.string({ message: "Required" });
z.string({ invalid_type_error: "Must be string" });
z.string({ required_error: "Required" });

After:

z.string({ error: "Required" });
z.string({ error: "Must be string" });
z.string({ error: "Required" });

Error Maps

Before:

z.object({}, { errorMap: customMap });

After:

z.object({}, { error: customMap });

Common Patterns

Nested Object Errors

const addressSchema = z.object({
  street: z.string({ error: "Street required" }),
  city: z.string({ error: "City required" }),
  zip: z.string({ error: "ZIP code required" })
});

const userSchema = z.object({
  name: z.string(),
  address: addressSchema
});

const result = userSchema.safeParse(data);
if (!result.success) {
  const errors = result.error.flatten();
}

Array Errors

const schema = z.array(
  z.object({
    name: z.string({ error: "Name required" })
  })
);

const result = schema.safeParse(data);
if (!result.success) {
  result.error.issues.forEach(issue => {
    console.log(`Index ${issue.path[0]}: ${issue.message}`);
  });
}

Union Errors

const schema = z.union([
  z.string(),
  z.number()
], {
  error: "Must be either string or number"
});

References

  • Validation: Use the validating-schema-basics skill from the zod-4 plugin
  • v4 Features: Use the validating-string-formats skill from the zod-4 plugin
  • Framework integration: Use the integrating-zod-frameworks skill from the zod-4 plugin
  • Testing: Use the testing-zod-schemas skill from the zod-4 plugin

Cross-Plugin References:

  • If displaying validation errors in React forms, use the validating-forms skill for error display patterns

Success Criteria

  • ✅ Using unified error parameter
  • ✅ SafeParse pattern for error handling
  • ✅ User-friendly error messages
  • ✅ Flattened errors for form display
  • ✅ Error maps for consistency
  • ✅ Proper error handling in UI frameworks
  • ✅ Test coverage for error paths