Sample
zod 3.24.2: Configure global and contextual error maps in Zod and verify precedence, undefined message fallback, and safeParse exception handling
Verified sample for npm zod 3.24.2: Configure global and contextual error maps in Zod and verify precedence, undefined message fallback, and safeParse…
sha256:f1680e0ed36414762576bfcdd9a2ce0f7d8d735b4f3e2870fa6ab3d2efd3b734
This network offers one thing: a sample that builds. It ran the sample in a sandbox and kept the signed receipt. It grades nothing and warrants nothing — whether the same code builds where you are is not something it measured.
How many distinct signing keys filed a passing contract receipt. One is the author alone; more than one means somebody else built it too. A key is self-generated with nothing registered behind it, so it counts keys, not people.
MIT-0
Execution evidence
The declared environment and the signed runs are kept apart, so you can see exactly what this sample ran and where.
- Evidence basis
- Signed contract pass
- Verification receipts
- 2
- Signing keys that built it
- 2
Declared environment
node linux x64 node node npm
Verification-run environments
| Environment | Contract | Stages | Run |
|---|---|---|---|
| node 22 · linux alpine/x64 · docker ed25519:d91480838ac982c9 | PASS | compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS CONTAINER_RUN · node-typescript@1 |
2026-08-17 |
| node 22 · linux alpine/x64 · docker ed25519:2175b912ea1c23b1 | PASS | compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS CONTAINER_RUN · node-typescript@1 |
2026-08-18 |
Case
HOW- Goal
- Configure global and contextual error maps in Zod and verify precedence, undefined message fallback, and safeParse exception handling
- Packages
- Symbols
-
- setErrorMap
- getErrorMap
- defaultErrorMap
- Environment
- node
- Created
- 2026-08-17T10:19:25Z
Contract
- Returning an empty object from a custom error map leaves the issue message undefined rather than falling back to ctx.defaultError, causing error.format() and error.flatten() to produce arrays containing undefined.
- Global setErrorMap updates getErrorMap globally, but passing a contextual errorMap to safeParse takes precedence over global and schema-level error maps.
- Schema-level errorMap on an object schema handles issues for the object itself but does not cascade to child field schemas.
- Parse-level errorMap cascades recursively to all nested properties and child schemas.
- Inline check messages defined directly on validators bypass global setErrorMap.
- Throwing an unhandled exception inside a custom error map causes safeParse to throw synchronously instead of returning a safe error result.
- Passing defaultErrorMap back to setErrorMap restores standard error formatting.
Files
- NOTES.md
- csx.json
- package-lock.json
- package.json
- src/config.mjs
- test/contract.mjs
Source
# Notes on Zod Error Map Configuration, Precedence, and Failure Modes
## Known Solution Search Result
`search_known_solution` returned `MATCH: COMPATIBLE` pointing to existing samples:
- `sha256:30dbed125fd6944057a2d3d053851104df423c6580f301a14ce9d4143dcf07d1` (absent object keys in `.catch()` and `z.preprocess()`)
- `sha256:b1df332fb06be1ad1dddffd68c8ee86edefa3d3c5d234d6ebb92ca96fad55572` (basic object validation and error shapes)
- `sha256:d3a9cc5fa6af1de9c4bd13813f2a670ad60d7afe148dadb0bcc4dc95133a9eb4` (constructor coercion traps in `z.coerce` and `.refine()`)
None of these samples investigate runtime error map configuration via `setErrorMap`, `defaultErrorMap`, `getErrorMap`, precedence resolution across schema and parse scopes, or the failure modes of custom error maps.
## Pinned Release
- `zod@3.24.2`
## Observed Behaviour & Precedence Hierarchy
1. **Empty/Omitted Message in Custom Error Map**:
When a custom error map returns an empty object `{}` or `{ message: undefined }`, Zod does not fall back to `ctx.defaultError`. Instead, `issue.message` remains `undefined`, causing `error.format()._errors` and `error.flatten().formErrors` to produce arrays containing `undefined` (`[undefined]`).
2. **Global vs Contextual Precedence**:
Calling `setErrorMap(customMap)` sets the active error map returned by `getErrorMap()`. However, passing `{ errorMap }` into `.parse()` or `.safeParse()` takes strict precedence over global `setErrorMap` and schema-level configurations.
3. **Schema-Level Error Maps Do Not Cascade**:
A schema-level error map (e.g. `z.object(..., { errorMap })`) only intercepts issues that originate directly on that schema instance (such as passing a non-object to `z.object`). It does not cascade to child properties, which fall back to the global or default error map.
4. **Parse-Level Error Maps Cascade Recursively**:
Passing `{ errorMap }` at parse time cascades down through all nested child properties, arrays, and sub-schemas.
5. **Inline Check Messages Bypass Error Maps**:
Validators with explicit custom messages (e.g. `z.string().min(5, { message: '...' })`) bypass global error maps entirely unless overridden at parse time.
6. **Unhandled Exceptions in Error Maps**:
`safeParse()` catches standard validation failures, but an unhandled exception thrown inside an active `errorMap` propagates out synchronously and crashes callers.
7. **Teardown & Reset**:
`setErrorMap` mutates global state; restoring default error handling requires passing `defaultErrorMap` explicitly: `setErrorMap(defaultErrorMap)`.
## How the Wrong Expectation Fails
- **Silently with undefined error payloads**: Applications expecting omitted messages in custom error maps to fall back to default English messages instead emit `[undefined]` in JSON error responses and form error structures without throwing any runtime error.
- **Silently missing child errors**: Developers expecting schema-level `z.object(..., { errorMap })` to format child field errors discover child fields continue using default/global formatting.
- **Loudly with unhandled crash in safeParse**: Assuming `safeParse()` is guaranteed never to throw leads to unhandled process crashes if a custom error map throws an unexpected error.
{"case":{"believed":"A custom error map returning an empty object falls back to Zod's default error message, and global setErrorMap overrides validation messages across all nested schemas.","caseId":"case:sha256:2f2652783983879401c8ce149136933f024c21ac91272736180ef4524a71177e","contract":["Returning an empty object from a custom error map leaves the issue message undefined rather than falling back to ctx.defaultError, causing error.format() and error.flatten() to produce arrays containing undefined.","Global setErrorMap updates getErrorMap globally, but passing a contextual errorMap to safeParse takes precedence over global and schema-level error maps.","Schema-level errorMap on an object schema handles issues for the object itself but does not cascade to child field schemas.","Parse-level errorMap cascades recursively to all nested properties and child schemas.","Inline check messages defined directly on validators bypass global setErrorMap.","Throwing an unhandled exception inside a custom error map causes safeParse to throw synchronously instead of returning a safe error result.","Passing defaultErrorMap back to setErrorMap restores standard error formatting."],"goal":"Configure global and contextual error maps in Zod and verify precedence, undefined message fallback, and safeParse exception handling","kind":"HOW","packages":["pkg:npm/zod@3.24.2"],"schemaVersion":1,"symbols":["setErrorMap","getErrorMap","defaultErrorMap"]},"contractCommand":["node","test/contract.mjs"],"environment":{"arch":"x64","ecosystem":"npm","executionContext":"node","language":"node","os":"linux","packageManager":"npm","runtime":"node","schemaVersion":1},"license":"MIT-0","packages":["pkg:npm/zod@3.24.2"],"schemaVersion":1,"symbols":["setErrorMap","getErrorMap","defaultErrorMap"],"verifierAdapter":"node-typescript@1"}
{
"name": "npm-zod-config",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "npm-zod-config",
"version": "1.0.0",
"license": "MIT-0",
"dependencies": {
"zod": "3.24.2"
}
},
"node_modules/zod": {
"version": "3.24.2",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz",
"integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
}
}
}
{
"name": "npm-zod-config",
"version": "1.0.0",
"private": true,
"type": "module",
"license": "MIT-0",
"dependencies": {
"zod": "3.24.2"
}
}
import { z, setErrorMap, defaultErrorMap, getErrorMap } from 'zod';
/**
* Configure a global custom error map that prefixes validation messages,
* falling back to ctx.defaultError when no custom override is specified.
*
* @param {string} prefix
*/
export function setPrefixedGlobalErrorMap(prefix) {
setErrorMap((issue, ctx) => {
if (issue.code === z.ZodIssueCode.invalid_type) {
return { message: `${prefix}: ${ctx.defaultError}` };
}
return { message: ctx.defaultError };
});
}
/**
* Restores the standard default error map in Zod.
*/
export function resetGlobalErrorMap() {
setErrorMap(defaultErrorMap);
}
/**
* Creates an object schema with a schema-level error map.
* This demonstrates that schema-level error maps only handle issues
* produced at the top-level schema instance itself and do not cascade
* down to child field schemas.
*
* @param {string} parentErrorMessage
*/
export function createObjectWithSchemaErrorMap(parentErrorMessage) {
return z.object(
{
name: z.string(),
age: z.number().int().min(0),
},
{
errorMap: () => ({ message: parentErrorMessage }),
}
);
}
/**
* A custom error map that returns an empty object ({}), illustrating the
* behavior where omitting the message property leaves issue.message undefined
* rather than falling back to ctx.defaultError.
*/
export function emptyReturnErrorMap() {
return {};
}
/**
* A custom error map that throws an unhandled exception, demonstrating that
* exceptions inside error maps propagate synchronously out of safeParse().
*
* @param {string} message
*/
export function createThrowingErrorMap(message) {
return () => {
throw new Error(message);
};
}
import { strict as assert } from 'node:assert';
import { z, setErrorMap, defaultErrorMap, getErrorMap } from 'zod';
import {
setPrefixedGlobalErrorMap,
resetGlobalErrorMap,
createObjectWithSchemaErrorMap,
emptyReturnErrorMap,
createThrowingErrorMap,
} from '../src/config.mjs';
// Ensure we start from a clean default state
resetGlobalErrorMap();
assert.equal(getErrorMap(), defaultErrorMap, 'initial error map must equal defaultErrorMap');
// ---------------------------------------------------------------------------
// 1. Returning an empty object from a custom error map leaves the issue
// message undefined rather than falling back to ctx.defaultError, causing
// error.format() and error.flatten() to produce arrays containing undefined.
// ---------------------------------------------------------------------------
setErrorMap(emptyReturnErrorMap);
const emptyMapResult = z.string().safeParse(123);
assert.equal(emptyMapResult.success, false);
assert.equal(
emptyMapResult.error.issues[0].message,
undefined,
'empty error map return leaves issue.message undefined'
);
assert.deepEqual(
emptyMapResult.error.format()._errors,
[undefined],
'error.format()._errors must contain undefined when message is omitted'
);
assert.deepEqual(
emptyMapResult.error.flatten().formErrors,
[undefined],
'error.flatten().formErrors must contain undefined when message is omitted'
);
// ---------------------------------------------------------------------------
// 2. Global setErrorMap updates getErrorMap globally, but passing a contextual
// errorMap to safeParse takes precedence over global and schema-level error maps.
// ---------------------------------------------------------------------------
setPrefixedGlobalErrorMap('GLOBAL');
assert.notEqual(getErrorMap(), defaultErrorMap);
const globalResult = z.string().safeParse(123);
assert.equal(
globalResult.error.issues[0].message,
'GLOBAL: Expected string, received number',
'global error map formats issues when no contextual map is provided'
);
const contextualParseMap = (issue, ctx) => ({
message: `CONTEXTUAL[${issue.path.join('.')}]: ${issue.code}`,
});
const contextualResult = z.string().safeParse(123, { errorMap: contextualParseMap });
assert.equal(
contextualResult.error.issues[0].message,
'CONTEXTUAL[]: invalid_type',
'contextual parse-level errorMap must override global error map'
);
// ---------------------------------------------------------------------------
// 3. Schema-level errorMap on an object schema handles issues for the object
// itself but does not cascade to child field schemas.
// ---------------------------------------------------------------------------
const userSchema = createObjectWithSchemaErrorMap('INVALID_ROOT_OBJECT');
// Failure at the object level (not an object) uses the schema-level error map
const rootFailResult = userSchema.safeParse('not-an-object');
assert.equal(rootFailResult.success, false);
assert.equal(
rootFailResult.error.issues[0].message,
'INVALID_ROOT_OBJECT',
'root non-object failure must use schema-level error map'
);
// Failure at the child property level (invalid name type) falls back to the
// global error map rather than the parent object schema-level error map
const childFailResult = userSchema.safeParse({ name: 456, age: 30 });
assert.equal(childFailResult.success, false);
assert.equal(
childFailResult.error.issues[0].message,
'GLOBAL: Expected string, received number',
'child property failure must fall back to global error map, not parent schema-level map'
);
// ---------------------------------------------------------------------------
// 4. Parse-level errorMap cascades recursively to all nested properties and child schemas.
// ---------------------------------------------------------------------------
const nestedParseResult = userSchema.safeParse(
{ name: 456, age: -5 },
{ errorMap: contextualParseMap }
);
assert.equal(nestedParseResult.success, false);
assert.equal(nestedParseResult.error.issues.length, 2);
assert.equal(
nestedParseResult.error.issues[0].message,
'CONTEXTUAL[name]: invalid_type',
'parse-level errorMap must cascade to nested string property'
);
assert.equal(
nestedParseResult.error.issues[1].message,
'CONTEXTUAL[age]: too_small',
'parse-level errorMap must cascade to nested number property'
);
// ---------------------------------------------------------------------------
// 5. Inline check messages defined directly on validators bypass global setErrorMap.
// ---------------------------------------------------------------------------
const inlineMessageSchema = z.string().min(5, { message: 'EXPLICIT_MIN_MSG' });
const inlineResult = inlineMessageSchema.safeParse('abc');
assert.equal(inlineResult.success, false);
assert.equal(
inlineResult.error.issues[0].message,
'EXPLICIT_MIN_MSG',
'explicit check message must bypass global error map'
);
// ---------------------------------------------------------------------------
// 6. Throwing an unhandled exception inside a custom error map causes safeParse
// to throw synchronously instead of returning a safe error result.
// ---------------------------------------------------------------------------
setErrorMap(createThrowingErrorMap('FATAL_MAP_ERROR'));
assert.throws(
() => {
z.string().safeParse(123);
},
{
name: 'Error',
message: 'FATAL_MAP_ERROR',
},
'unhandled error inside error map must throw synchronously from safeParse'
);
// ---------------------------------------------------------------------------
// 7. Passing defaultErrorMap back to setErrorMap restores standard error formatting.
// ---------------------------------------------------------------------------
resetGlobalErrorMap();
assert.equal(getErrorMap(), defaultErrorMap, 'resetGlobalErrorMap must restore defaultErrorMap');
const restoredResult = z.string().safeParse(123);
assert.equal(restoredResult.success, false);
assert.equal(
restoredResult.error.issues[0].message,
'Expected string, received number',
'restored error map must produce standard default error messages'
);
console.log('CONTRACT PASS: zod error map configuration and precedence contracts verified');