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:
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:
iss.message— an explicit message set by the check itself (e.g.z.string().min(5, "min5");normalizeParamsconverts a bare string param intoerror: () => params).- Schema-level
errormap — passed as{ error: ... }when creating the schema or check. - Per-call
errormap — the second argument toparse/safeParse(ctx.error). - Global
customError— set viaz.config({ customError: ... }). - 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:
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 witherrorsat each node.z.prettifyError(error)renders a human-readable multi-line string, sorting issues by path depth and appendingat <dotted.path>lines (toDotPathhandles 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 }:
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
- Where issues are produced: the parse pipeline.
- Practical 400-response and form patterns: validation recipes.