Capítulo 42 de 61

Chapter 42: Global Error Customization, Internationalization & Precedence

Core Idea

Beyond schema-level and per-parse error messages, Zod supports a global custom error map and 50+ built-in locale packs, all resolved through one well-defined precedence chain from most to least specific.

Key Concepts

  • z.config({ customError: (iss) => ... }): sets a global fallback error map, lowest-precedence of the custom-message mechanisms (still above locale defaults).
  • Locales: zod auto-loads the en locale; Zod Mini loads none by default (falls back to a generic "Invalid input" message for everything). Import and apply one via z.config(en()) or z.config(z.locales.en()); dynamic import() supports lazy-loading a locale by string name.
  • esbuild tree-shaking caveat: import * as z from "zod" tree-shakes unused locales correctly with Rollup/Webpack; esbuild can't tree-shake the import { z } from "zod" / import z from "zod" forms and bundles every locale regardless — prefer the namespace import form when using esbuild.
  • 50+ locales available, covering major world languages and regional variants (e.g. separate pt/ptBR, zhCN/zhTW, fr/frCA).
  • Error precedence, highest to lowest: (1) check-level error (e.g. .min(5, "Too short!")), (2) schema-level error (e.g. z.string("Invalid name"), applies to any issue from that schema lacking its own check-level message), (3) per-parse error map, (4) global error map (z.config({ customError })), (5) locale error map.

Code Examples

z.config({
  customError: (iss) => {
    if (iss.code === "invalid_type") return `invalid type, expected ${iss.expected}`;
    if (iss.code === "too_small") return `minimum is ${iss.minimum}`;
  },
});

import * as z from "zod";
import { en } from "zod/locales";
z.config(en()); // or z.config(z.locales.en())

async function loadLocale(locale: string) {
  const mod = await import(`zod/v4/locales/${locale}.js`);
  z.config(mod.default());
}
await loadLocale("fr");

// precedence demonstration: check-level wins over schema-level
z.string("Invalid name").min(5).safeParse("ab"); // => "Invalid name" (no check-level override was set)
z.string().min(5, "Too short!").safeParse("ab"); // => "Too short!" (check-level)
  • What it demonstrates: applying and lazily loading a locale, and how a global error map is written for multiple issue codes.

Key Takeaways

  1. The precedence chain (check > schema > per-parse > global > locale) means a locale is always the fallback of last resort — set it once at app startup, then override specific messages closer to where they're defined.
  2. Zod Mini's lack of an auto-loaded locale means every message is a generic "Invalid input" unless you explicitly configure one — don't assume Mini behaves like full Zod here.
  3. If bundling with esbuild specifically, use import * as z from "zod" to avoid accidentally shipping every locale in the bundle.

Connects To

  • The error Param & Schema-Level Customization: the higher-precedence, more specific customization mechanisms this chapter's global/locale layer falls back to.