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:

packages/zod/src/v4/core/errors.ts
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;
}
  • origin disambiguates what "too small" means — a short string, a small number, an under-filled array — so one code covers every sized type.
  • invalid_union is two shapes under one code: the no-match case carries every option's issues in errors; the multiple-match case (discriminated unions that require exclusivity) carries inclusive: 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 by at <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/.

Sources: packages/zod/src/v4/core/errors.ts · last synced 2026-08-10 · 2d90846 · version 4.4.3