How to Handle and Customize Zod Parsing Errors: A Complete Guide
Zod exposes validation failures as ZodError instances containing an issues array, and you can customize messages at four precedence levels—schema, parse-time, global, or locale—using error maps that inspect issue codes and context.
Zod (colinhacks/zod) is a TypeScript-first schema validation library that generates detailed error reports when parsing fails. Understanding how to handle and customize Zod parsing errors lets you transform raw validation issues into user-friendly messages, structured API responses, or localized form errors without manually iterating complex nested arrays.
Understanding the ZodError Class and Issue Structure
In packages/zod/src/v4/core/errors.ts (lines 15‑27), Zod defines the $ZodError class (exposed as z.ZodError) which stores validation failures in an issues array. Each issue implements $ZodIssueBase containing code (a string discriminator), path (location array), message (description), and an optional input property when enabled.
The full issue union type $ZodIssue (lines 10‑95) includes specific variants such as $ZodIssueInvalidType (with expected and received fields), $ZodIssueTooBig (with maximum), and $ZodIssueTooSmall (with minimum). This discriminated union structure allows error maps to generate context-aware messages by inspecting iss.code and related metadata.
The Four Levels of Error Customization
According to packages/docs/content/error-customization.mdx, Zod applies error messages in strict precedence order. When an error map returns undefined or null, Zod falls back to the next level.
1. Schema-Level Messages (Highest Priority)
Override messages when defining schemas by passing a string or error map to individual validators:
import * as z from "zod";
const schema = z.object({
username: z.string("Username must be a string"),
age: z
.number()
.int({ error: "Age must be an integer" })
.min(0, { error: "Age cannot be negative" }),
});
2. Per-Parse Error Maps
Provide an error option to parse() or safeParse() for case-specific handling that overrides schema defaults but yields to global configuration:
const result = schema.safeParse(
{ username: 123, age: -5 },
{
error: (iss) => {
if (iss.code === "invalid_type") return "Wrong type supplied";
// Returning undefined falls back to schema-level messages
},
}
);
3. Global Error Configuration
Call z.config() once to establish application-wide error handling (lower precedence than per-parse maps):
z.config({
customError: (iss) => {
if (iss.code === "invalid_type")
return `Expected ${iss.expected}, got ${typeof iss.input}`;
// Return undefined to fall back to schema messages or locales
},
});
4. Locale-Based Messages (Lowest Priority)
Load built-in locales for translated defaults without writing custom logic. Locales are overridden by any higher-precedence custom messages:
import { fr } from "zod/locales";
z.config(fr()); // Default messages now render in French
Transforming Errors with Utility Helpers
The same packages/zod/src/v4/core/errors.ts file exports three primary utilities (implemented across lines 62‑86, 90‑126, 138‑165, and 335‑447) to restructure error data for different consumption patterns.
flattenError()
Converts a ZodError to a shallow object with formErrors (root-level issues) and fieldErrors (path-mapped arrays), ideal for form libraries like React Hook Form or Formik:
const result = schema.safeParse({ username: 123, age: -5 });
if (!result.success) {
const flat = z.flattenError(result.error);
console.log(flat.fieldErrors.username); // ["Username must be a string"]
console.log(flat.fieldErrors.age); // ["Age must be an integer", "Age cannot be negative"]
}
treeifyError()
Builds a nested object mirroring your schema shape, separating top-level errors from nested properties (for objects) or items (for arrays). This structure suits recursive UI components that need to display errors alongside specific form fields:
if (!result.success) {
const errTree = z.treeifyError(result.error);
// errTree.properties?.username?.errors => ["Username must be a string"]
// errTree.properties?.age?.errors => ["Age must be an integer", ...]
}
prettifyError()
Generates a human-readable multiline string with dot-path locations, useful for logging, CLI output, or debugging:
console.log(z.prettifyError(result.error));
/*
✖ Username must be a string
✖ Age cannot be negative
→ at age
*/
Including Raw Input in Error Reports
By default, Zod strips raw input values from the issues payload for privacy. Set reportInput: true in parse options (documented in packages/docs/content/error-customization.mdx, lines 40‑57) to expose the actual value that failed validation:
const result = schema.safeParse(
{ username: 123 },
{ reportInput: true }
);
if (!result.success) {
console.log(result.error.issues[0].input); // 123
}
Complete Implementation Examples
Basic Error Handling with safeParse
import * as z from "zod";
const userSchema = z.object({
email: z.string().email(),
age: z.number().int().min(18),
});
const result = userSchema.safeParse({ email: "invalid", age: 16 });
if (!result.success) {
// Access the raw issues array
console.log(result.error.issues);
// Transform for UI
const tree = z.treeifyError(result.error);
const flat = z.flattenError(result.error);
const pretty = z.prettifyError(result.error);
}
Context-Aware Global Error Map
z.config({
customError: (iss) => {
switch (iss.code) {
case "too_small":
return `Value must be at least ${iss.minimum}`;
case "too_big":
return `Value must be at most ${iss.maximum}`;
case "invalid_format":
return `Invalid format: expected ${iss.format}`;
default:
return undefined; // Fall back to schema messages
}
},
});
Combining Locales with Custom Overrides
import { de } from "zod/locales";
// Set German as baseline
z.config(de());
// Override specific fields at schema level
const schema = z.object({
password: z.string({ error: "Passwort erforderlich" }).min(8, { error: "Mindestens 8 Zeichen" }),
});
Summary
- ZodError structure: Validation failures are instances of
z.ZodErrorcontaining anissuesarray where each issue hascode,path,message, and optionalinputproperties defined inpackages/zod/src/v4/core/errors.ts. - Customization precedence: Schema-level messages override per-parse maps, which override global
z.config(), which override locale files. - Error maps: Functions receive a discriminated union issue object (
$ZodIssue) and return a string orundefinedto delegate to the next level. - Formatting utilities: Use
z.flattenError()for form libraries,z.treeifyError()for nested UI components, andz.prettifyError()for readable logging. - Privacy controls: Raw input values appear in issues only when explicitly enabling
reportInput: trueduring parsing.
Frequently Asked Questions
How do I access the specific error code in a ZodError?
Each issue in the issues array contains a code property that acts as a discriminator. You can inspect this in error maps or when iterating over result.error.issues. Common codes include "invalid_type", "too_small", "too_big", and "invalid_format", each with additional context like minimum, maximum, or expected fields.
Can I customize error messages based on the path?
Yes. In any error map (schema, parse, or global), inspect the path array on the issue object. For example, if (iss.path[0] === "password") return "Custom password message" lets you target specific fields while using generic logic for others.
What is the difference between flattenError and treeifyError?
z.flattenError() produces a shallow structure with formErrors for root issues and fieldErrors mapped by path strings, optimized for flat form field associations. z.treeifyError() creates a deeply nested object that mirrors your schema structure using properties for object keys and items for array indices, better suited for recursive component trees.
How do I show the actual value that caused the validation error?
Pass { reportInput: true } as the second argument to parse() or safeParse(). This adds an input property to each issue object containing the raw value that failed validation. By default, Zom omits this field to prevent accidental logging of sensitive data like passwords or tokens.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →