ZodError Structure
When validation fails, Zod throws a ZodError that contains detailed information about all validation issues.
Basic Error Structure
import * as z from 'zod';
const schema = z.object({
f1: z.number(),
f3: z.string().nullable(),
f4: z.array(z.object({ t: z.union([z.string(), z.boolean()]) }))
});
const result = schema.safeParse({});
if (!result.success) {
console.log(result.error);
// ZodError with issues array
}
The ZodError object contains:
issues: Array of $ZodIssue objects describing each validation failure
message: JSON stringified representation of all issues
name: Always "ZodError"
Issue Types
Zod provides different issue types for various validation failures:
Invalid Type Issue
interface $ZodIssueInvalidType {
readonly code: "invalid_type";
readonly expected: "string" | "number" | "boolean" | "array" | "object" | ...;
readonly input?: unknown;
readonly path: PropertyKey[];
readonly message: string;
}
Example:
const result = z.string().safeParse(234);
// {
// "expected": "string",
// "code": "invalid_type",
// "path": [],
// "message": "Invalid input: expected string, received number"
// }
Too Small/Too Big Issues
const result = z.array(z.string()).min(3).safeParse(["asdf", "qwer"]);
if (!result.success) {
console.log(result.error.issues[0]);
// {
// "origin": "array",
// "code": "too_small",
// "minimum": 3,
// "inclusive": true,
// "path": [],
// "message": "Too small: expected array to have >=3 items"
// }
}
const schema = z.string().email();
const result = schema.safeParse("asdfsdf");
// Issue: { code: "invalid_format", format: "email", ... }
Custom Issue
const schema = z.number().refine((val) => val > 3, {
message: "override"
});
const result = schema.safeParse(2);
// {
// "code": "custom",
// "path": [],
// "message": "override"
// }
Zod provides multiple ways to format errors for different use cases.
Flattened Errors
The flatten() method groups errors into form-level and field-level errors:
const Test = z.object({
f1: z.number(),
f2: z.string().optional(),
f3: z.string().nullable(),
f4: z.array(z.object({ t: z.union([z.string(), z.boolean()]) }))
});
const result = Test.safeParse({});
const flattened = result.error!.flatten();
console.log(flattened);
// {
// "fieldErrors": {
// "f1": ["Invalid input: expected number, received undefined"],
// "f3": ["Invalid input: expected string, received undefined"],
// "f4": ["Invalid input: expected array, received undefined"]
// },
// "formErrors": []
// }
Custom Flatten Mapper
You can provide a custom mapper function to transform issues:
type ErrorType = { message: string; code: number };
const flattened = result.error!.flatten((iss) => ({
message: iss.message,
code: 1234
}));
// Returns ErrorType[] instead of string[]
The format() method creates a nested tree structure matching your schema:
const schema = z.object({
inner: z.object({
name: z.string().refine((val) => val.length > 5).array()
.refine((val) => val.length <= 1)
})
});
const result = schema.safeParse({
inner: { name: ["aasd", "asdfasdfasfd"] }
});
const formatted = result.error!.format();
// {
// "_errors": [],
// "inner": {
// "_errors": [],
// "name": {
// "_errors": ["Invalid input"],
// // ... nested errors
// }
// }
// }
const schema = z.string().email();
const result = schema.safeParse("asdfsdf");
const formatted = result.error!.format();
// {
// "_errors": ["Invalid email address"]
// }
Custom Error Messages
String Parameter
The simplest way to customize error messages:
// For schema constructors
const schema = z.string("Bad!");
schema.safeParse(123).error!.issues[0].message; // "Bad!"
// For refinements
const refined = z.number().refine((x) => x > 3, "override");
refined.safeParse(2).error!.issues[0].message; // "override"
// For checks
const minLength = z.string().min(5, "Too short!");
minLength.safeParse("abc").error!.issues[0].message; // "Too short!"
Message Object
const schema = z.string().min(10, { message: "override" });
const result = schema.safeParse("tooshort");
// message: "override"
Refinement with Custom Path
const schema = z.object({
password: z.string(),
confirm: z.string()
}).refine((val) => val.confirm === val.password, {
path: ["confirm"],
message: "Passwords don't match"
});
const result = schema.safeParse({
password: "peanuts",
confirm: "qeanuts"
});
const error = result.error!.format();
// error.confirm._errors = ["Passwords don't match"]
Refinement with Params
const schema = z.number().refine((val) => val >= 3, {
params: { minimum: 3 }
});
const result = schema.safeParse(2);
// {
// "code": "custom",
// "path": [],
// "params": { "minimum": 3 },
// "message": "Invalid input"
// }
Use params to pass additional metadata to error maps for dynamic message generation.
Error Utilities
Adding Issues
const err = new z.ZodError([]);
err.addIssue({
code: "invalid_type",
expected: "object",
path: [],
message: "Expected object",
input: "adf"
});
// Or add multiple issues
err.addIssues([issue1, issue2]);
Checking Empty Errors
if (err.isEmpty) {
// No validation errors
}
Don’t call .parse() on schemas with async refinements. It will throw an error. Use .parseAsync() instead.
TypeScript Type Inference
type FormattedError = z.inferFormattedError<typeof schema>;
type FlattenedError = z.inferFlattenedErrors<typeof schema>;
// With custom mapper
type CustomFormatted = z.inferFormattedError<typeof schema, number>;
Best Practices
- Use
safeParse in production - Returns a result object instead of throwing
- Flatten for forms - Use
.flatten() for form validation to map errors to fields
- Format for nested data - Use
.format() for complex nested objects
- Add context with params - Include
params in refinements for dynamic error messages
- Set custom paths - Use the
path option in refinements to target specific fields