colinhacks/zod · error · Error

Cannot specify both `message` and `error` params

Error message

Cannot specify both `message` and `error` params

What it means

Thrown by normalizeParams() when a single params object sets both `message` and `error`. Zod v4 unifies these into one `error` channel (a string or a function), so `message` is treated as a legacy alias that gets rewritten into `error`. Supplying both is ambiguous and rejected at parameter-normalization time, before any parsing happens. It fires across most schema/check/error-map APIs that accept a params object.

Solutions

  1. Pick one key: keep `error` (string or function) and delete the `message` property.
  2. If you only need a static string, use `message` alone (it is rewritten to `error` internally) or pass the string directly as the params value.
  3. When merging config objects, normalize each source to `error` before spreading so the combined object never carries both keys.

Example fix

// before
z.string().min(3, { message: "too short", error: (iss) => `bad: ${iss.input}` });

// after (function form)
z.string().min(3, { error: (iss) => `bad: ${iss.input}` });
// after (static string form)
z.string().min(3, { message: "too short" });
Defensive patterns

Strategy: validation

Validate before calling

function assertNoMessageErrorConflict(params: unknown): void {
  if (params && typeof params === 'object') {
    const p = params as Record<string, unknown>;
    if (p.message !== undefined && p.error !== undefined) {
      throw new TypeError(
        'params contains both `message` and `error`; keep only `error` (string or function)'
      );
    }
  }
}
// run before any zod call that accepts a params object:
// assertNoMessageErrorConflict(myParams);

Type guard

function hasMessageAndError(p: unknown): p is { message: unknown; error: unknown } {
  if (!p || typeof p !== 'object') return false;
  const o = p as Record<string, unknown>;
  return o.message !== undefined && o.error !== undefined;
}

Prevention

When it happens

Trigger: Passing an object literal like `{ message: "...", error: (iss) => "..." }` to any API that runs its params through normalizeParams() — e.g. `z.string().check({ message: "x", error: () => "y" })`, custom error maps, or `.refine()` where a refine carries both keys. Also triggered by spreading two config sources that each define one of the keys.

Common situations: Migrating a v3 codebase where `message` was the only option while leaving a newly added v4 `error` fn in place; merging a base error-map with an override via spread; copy-pasting an example that mixed the two syntaxes; library wrappers that auto-inject `message` and then the user adds `error`.

Related errors


AI-assisted analysis of colinhacks/zod@2d90846af9 (2026-08-11). Data as JSON: /api/errors/a09b277da74796bf. Report an issue: GitHub.

Appendix: source

Thrown at packages/zod/src/v4/core/util.ts:523

        {
          [k in keyof Omit<T, "error" | "message">]: T[k];
        } & ("error" extends keyof T
          ? {
              error?: Exclude<T["error"], string>;
              // path?: PropertyKey[] | undefined;
              // message?: string | undefined;
            }
          : unknown)
      >
    : never;

export function normalizeParams<T>(_params: T): Normalize<T> {
  const params: any = _params;

  if (!params) return {} as any;
  if (typeof params === "string") return { error: () => params } as any;
  if (params?.message !== undefined) {
    if (params?.error !== undefined) throw new Error("Cannot specify both `message` and `error` params");
    params.error = params.message;
  }
  delete params.message;
  if (typeof params.error === "string") return { ...params, error: () => params.error } as any;
  return params;
}

export function createTransparentProxy<T extends object>(getter: () => T): T {
  let target: T;
  return new Proxy(
    {},
    {
      get(_, prop, receiver) {
        target ??= getter();
        return Reflect.get(target, prop, receiver);
      },
      set(_, prop, value, receiver) {
        target ??= getter();

View on GitHub (pinned to 2d90846af9)