Every failed parse produces one ZodError carrying an issues array. Each issue has a code telling you what kind of failure it was, a path locating it in the input, and a human-readable message. All of it is defined in packages/zod/src/v4/core/errors.ts, shared by Classic and Mini — the error you catch is the same object either way.
Issue codes
The twelve codes, from the $ZodIssue* interface definitions:
| Code | Fired when | Extra fields |
|---|---|---|
invalid_type |
Input is the wrong primitive/structural type | expected (one of "string", "number", "object", … per $ZodInvalidTypeExpected) |
too_big |
Exceeds a maximum (z.max, z.length, …) |
origin, maximum, inclusive, exact |
too_small |
Below a minimum (z.min, z.nonempty, …) |
origin, minimum, inclusive, exact |
invalid_format |
A string format check failed (email, uuid, url, …) |
format, pattern |
not_multiple_of |
z.multipleOf failed |
divisor |
unrecognized_keys |
A strict object received unknown keys | keys |
invalid_union |
No union option matched (or, for exclusive unions, more than one did) | errors (per-option issue arrays), discriminator |
invalid_key |
A map/record key failed its schema | origin, issues |
invalid_element |
A map/set element failed its schema | origin, key, issues |
invalid_value |
Value not in an enum/literal set | values |
custom |
A refine/superRefine/check reported failure |
params |
Two details worth knowing from the source:
export interface $ZodIssueTooSmall<Input = unknown> extends $ZodIssueBase {
readonly code: "too_small";
readonly origin: "number" | "int" | "bigint" | "date" | "string" | "array" | "set" | "file" | (string & {});
readonly minimum: number | bigint;
/** True if the allowable range includes the minimum */
readonly inclusive?: boolean;
/** True if the allowed value is fixed (e.g.` z.length(5)`), not a range (`z.minLength(5)`) */
readonly exact?: boolean;
readonly input?: Input;
}
origindisambiguates what "too small" means — a short string, a small number, an under-filled array — so one code covers every sized type.invalid_unionis two shapes under one code: the no-match case carries every option's issues inerrors; the multiple-match case (discriminated unions that require exclusivity) carriesinclusive: false.
Turning an error into output
Four helpers, all exported from the package root:
z.prettifyError(error)— a terminal-friendly multi-line string; issues sorted by path depth, each line followed byat <dot.path>.z.flattenError(error)—{ formErrors, fieldErrors }, the classic shape for one-level form validation.z.treeifyError(error)— a nested tree mirroring the schema, for deeply structured UIs.z.formatError(error)— the Zod 3.format()shape, kept for migrations.
flattenError and treeifyError accept an optional mapper (issue) => U when you need more than the message string per issue.
Customizing messages
Messages come from a locale layer, installed at import time (English by default — see ../public-api/). Per-check overrides take an error param at the call site; whole-application overrides go through z.config() with a different locale from zod/locales. The path an issue's message travels — from check failure to localized string — is traced in ../../how-it-works/error-pipeline/.