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
- Pick one key: keep `error` (string or function) and delete the `message` property.
- 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.
- 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
- Standardize on the `error` key (string or function) across the codebase; treat `message` as legacy.
- When merging error maps, normalize each source to `error` before spreading.
- Add an ESLint rule or wrapper that rejects params objects containing both keys.
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
- This schema contains multiple valid literal values. Use…
- Unrecognized hash format
- Can't use "invalid_type_error" or "required_error" in…
- Cannot overwrite keys on object schemas containing…
- Invalid hex string length
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)