Zod never throws inside the parse pipeline. Failures accumulate as plain issue objects on the payload, and only the outermost driver converts them into an error object. This page covers that conversion and the utilities for presenting the result.

Raw issues vs final issues

While parsing, an issue looks like { code: "too_big", maximum: 5, input, inst, continue: false } — it references the live schema instance and the offending input. finalizeIssue (called once per issue in core/parse.ts) strips the internal fields, fills in path and message, and produces the public $ZodIssue:

packages/zod/src/v4/core/util.ts
export function finalizeIssue(
  iss: errors.$ZodRawIssue,
  ctx: schemas.ParseContextInternal | undefined,
  config: $ZodConfig
): errors.$ZodIssue {
  const message = iss.message
    ? iss.message
    : (unwrapMessage(iss.inst?._zod.def?.error?.(iss as never)) ??
      unwrapMessage(ctx?.error?.(iss as never)) ??
      unwrapMessage(config.customError?.(iss)) ??
      unwrapMessage(config.localeError?.(iss)) ??
      "Invalid input");

  const { inst: _inst, continue: _continue, input: _input, ...rest } = iss as any;
  rest.path ??= [];
  rest.message = message;
  if (ctx?.reportInput) {
    rest.input = _input;
  }
  return rest;
}

The ?? chain is the entire error-customization model, highest priority first:

  1. iss.message — an explicit message set by the check itself (e.g. z.string().min(5, "min5"); normalizeParams converts a bare string param into error: () => params).
  2. Schema-level error map — passed as { error: ... } when creating the schema or check.
  3. Per-call error map — the second argument to parse/safeParse (ctx.error).
  4. Global customError — set via z.config({ customError: ... }).
  5. The active locale's localeError, then the hard-coded "Invalid input".

Note that input is omitted from final issues unless the caller opts in with reportInput: true — error objects are safe to serialize into logs and API responses without leaking payloads by default.

ZodError is Error-like, twice

The classic layer defines the error class with the same trait system as schemas, and in two variants:

packages/zod/src/v4/classic/errors.ts
export const ZodError: core.$constructor<ZodError> = /*@__PURE__*/ core.$constructor("ZodError", initializer);
export const ZodRealError: core.$constructor<ZodError> = /*@__PURE__*/ core.$constructor("ZodError", initializer, {
  Parent: Error,
});

ZodRealError — the one actually thrown, wired in via core._parse(ZodRealError) in classic/parse.ts — extends Error for proper stack traces. The plain ZodError trait exists so instanceof ZodError works structurally across package copies. The driver also calls util.captureStackTrace(e, callee) so the stack starts at your .parse() call rather than inside Zod.

Presenting errors

error.issues is the source of truth: an array of { code, path, message, ... }. Three helpers in core/errors.ts reshape it, all re-exported from the package root:

  • z.flattenError(error) splits issues into { formErrors: string[], fieldErrors: { [key]: string[] } } — one level deep, ideal for form UIs.
  • z.treeifyError(error) mirrors the schema's nesting as a tree with errors at each node.
  • z.prettifyError(error) renders a human-readable multi-line string, sorting issues by path depth and appending at <dotted.path> lines (toDotPath handles bracket-quoting of unusual keys).

The deprecated v3-style methods still exist on the instance — error.format() and error.flatten() delegate to formatError/flattenError, and their JSDoc points at z.treeifyError as the replacement (classic/errors.ts).

Localization

A locale is just a function returning { localeError: $ZodErrorMap }:

packages/zod/src/v4/locales/en.ts
export default function (): { localeError: errors.$ZodErrorMap } {
  return {
    localeError: error(),
  };
}

The English map builds messages from issue metadata — for example sized types get per-origin units (string: { unit: "characters", verb: "to have" }) and string formats get display names (email: "email address", jwt: "JWT"). There are 52 locale files under packages/zod/src/v4/locales/, each independently importable (zod/v4/locales/de). Classic Zod activates English at import time; switching is one call: z.config(z.locales.de()). Configuration details are on configuration and locales.

Related pages

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