Skip to content

Commit d55122e

Browse files
committed
fix(v4): preserve callsites in parse stack traces
1 parent 1fc9f31 commit d55122e

4 files changed

Lines changed: 149 additions & 46 deletions

File tree

packages/zod/src/v4/classic/parse.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,25 +36,29 @@ export const safeParseAsync: <T extends core.$ZodType>(
3636
export const encode: <T extends core.$ZodType>(
3737
schema: T,
3838
value: core.output<T>,
39-
_ctx?: core.ParseContext<core.$ZodIssue>
39+
_ctx?: core.ParseContext<core.$ZodIssue>,
40+
_params?: { callee?: core.util.AnyFunc; Err?: core.$ZodErrorClass }
4041
) => core.input<T> = /* @__PURE__ */ core._encode(ZodRealError);
4142

4243
export const decode: <T extends core.$ZodType>(
4344
schema: T,
4445
value: core.input<T>,
45-
_ctx?: core.ParseContext<core.$ZodIssue>
46+
_ctx?: core.ParseContext<core.$ZodIssue>,
47+
_params?: { callee?: core.util.AnyFunc; Err?: core.$ZodErrorClass }
4648
) => core.output<T> = /* @__PURE__ */ core._decode(ZodRealError);
4749

4850
export const encodeAsync: <T extends core.$ZodType>(
4951
schema: T,
5052
value: core.output<T>,
51-
_ctx?: core.ParseContext<core.$ZodIssue>
53+
_ctx?: core.ParseContext<core.$ZodIssue>,
54+
_params?: { callee?: core.util.AnyFunc; Err?: core.$ZodErrorClass }
5255
) => Promise<core.input<T>> = /* @__PURE__ */ core._encodeAsync(ZodRealError);
5356

5457
export const decodeAsync: <T extends core.$ZodType>(
5558
schema: T,
5659
value: core.input<T>,
57-
_ctx?: core.ParseContext<core.$ZodIssue>
60+
_ctx?: core.ParseContext<core.$ZodIssue>,
61+
_params?: { callee?: core.util.AnyFunc; Err?: core.$ZodErrorClass }
5862
) => Promise<core.output<T>> = /* @__PURE__ */ core._decodeAsync(ZodRealError);
5963

6064
export const safeEncode: <T extends core.$ZodType>(

packages/zod/src/v4/classic/schemas.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -237,10 +237,10 @@ export const ZodType: core.$constructor<ZodType> = /*@__PURE__*/ core.$construct
237237
inst.parseAsync = async (data, params) => parse.parseAsync(inst, data, params, { callee: inst.parseAsync });
238238
inst.safeParseAsync = async (data, params) => parse.safeParseAsync(inst, data, params);
239239
inst.spa = inst.safeParseAsync;
240-
inst.encode = (data, params) => parse.encode(inst, data, params);
241-
inst.decode = (data, params) => parse.decode(inst, data, params);
242-
inst.encodeAsync = async (data, params) => parse.encodeAsync(inst, data, params);
243-
inst.decodeAsync = async (data, params) => parse.decodeAsync(inst, data, params);
240+
inst.encode = (data, params) => parse.encode(inst, data, params, { callee: inst.encode });
241+
inst.decode = (data, params) => parse.decode(inst, data, params, { callee: inst.decode });
242+
inst.encodeAsync = async (data, params) => parse.encodeAsync(inst, data, params, { callee: inst.encodeAsync });
243+
inst.decodeAsync = async (data, params) => parse.decodeAsync(inst, data, params, { callee: inst.decodeAsync });
244244
inst.safeEncode = (data, params) => parse.safeEncode(inst, data, params);
245245
inst.safeDecode = (data, params) => parse.safeDecode(inst, data, params);
246246
inst.safeEncodeAsync = async (data, params) => parse.safeEncodeAsync(inst, data, params);

packages/zod/src/v4/classic/tests/error.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,36 @@ import { inspect } from "node:util";
22
import { expect, test } from "vitest";
33
import * as z from "zod/v4";
44

5+
function getThrownError(fn: () => unknown): unknown {
6+
try {
7+
fn();
8+
} catch (error) {
9+
return error;
10+
}
11+
throw new Error("Expected function to throw");
12+
}
13+
14+
async function getRejectedError(fn: () => Promise<unknown>): Promise<unknown> {
15+
try {
16+
await fn();
17+
} catch (error) {
18+
return error;
19+
}
20+
throw new Error("Expected function to reject");
21+
}
22+
23+
function expectFirstStackFrameAtCallsite(error: unknown): void {
24+
expect(error).toBeInstanceOf(Error);
25+
const stack = (error as Error).stack;
26+
expect(stack).toEqual(expect.any(String));
27+
28+
const firstFrame = stack!.split("\n").find((line) => line.trim().startsWith("at "));
29+
expect(firstFrame).toBeDefined();
30+
expect(firstFrame).toContain("error.test.ts");
31+
expect(firstFrame).not.toContain("core/parse.ts");
32+
expect(firstFrame).not.toContain("classic/schemas.ts");
33+
}
34+
535
test("error creation", () => {
636
const err1 = new z.ZodError([]);
737

@@ -683,6 +713,44 @@ test("error inheritance", () => {
683713
}
684714
});
685715

716+
test("parse errors capture the caller stack frame", async () => {
717+
const schema = z.string();
718+
const parse = schema.parse;
719+
const parseAsync = schema.parseAsync;
720+
721+
expectFirstStackFrameAtCallsite(getThrownError(() => schema.parse(123)));
722+
expectFirstStackFrameAtCallsite(getThrownError(() => parse(123)));
723+
expectFirstStackFrameAtCallsite(getThrownError(() => z.parse(schema, 123)));
724+
725+
expectFirstStackFrameAtCallsite(await getRejectedError(() => schema.parseAsync(123)));
726+
expectFirstStackFrameAtCallsite(await getRejectedError(() => parseAsync(123)));
727+
expectFirstStackFrameAtCallsite(await getRejectedError(() => z.parseAsync(schema, 123)));
728+
});
729+
730+
test("codec errors capture the caller stack frame", async () => {
731+
const schema = z.string();
732+
const encode = schema.encode;
733+
const decode = schema.decode;
734+
const encodeAsync = schema.encodeAsync;
735+
const decodeAsync = schema.decodeAsync;
736+
737+
expectFirstStackFrameAtCallsite(getThrownError(() => schema.encode(123 as any)));
738+
expectFirstStackFrameAtCallsite(getThrownError(() => encode(123 as any)));
739+
expectFirstStackFrameAtCallsite(getThrownError(() => z.encode(schema, 123 as any)));
740+
741+
expectFirstStackFrameAtCallsite(getThrownError(() => schema.decode(123 as any)));
742+
expectFirstStackFrameAtCallsite(getThrownError(() => decode(123 as any)));
743+
expectFirstStackFrameAtCallsite(getThrownError(() => z.decode(schema, 123 as any)));
744+
745+
expectFirstStackFrameAtCallsite(await getRejectedError(() => schema.encodeAsync(123 as any)));
746+
expectFirstStackFrameAtCallsite(await getRejectedError(() => encodeAsync(123 as any)));
747+
expectFirstStackFrameAtCallsite(await getRejectedError(() => z.encodeAsync(schema, 123 as any)));
748+
749+
expectFirstStackFrameAtCallsite(await getRejectedError(() => schema.decodeAsync(123 as any)));
750+
expectFirstStackFrameAtCallsite(await getRejectedError(() => decodeAsync(123 as any)));
751+
expectFirstStackFrameAtCallsite(await getRejectedError(() => z.decodeAsync(schema, 123 as any)));
752+
});
753+
686754
test("error serialization", () => {
687755
try {
688756
z.string().parse(123);

packages/zod/src/v4/core/parse.ts

Lines changed: 69 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,35 @@ import type * as schemas from "./schemas.js";
44
import * as util from "./util.js";
55

66
export type $ZodErrorClass = { new (issues: errors.$ZodIssue[]): errors.$ZodError };
7+
type $ParseParams = { callee?: util.AnyFunc; Err?: $ZodErrorClass };
8+
9+
function finalizeParams(callee: util.AnyFunc, params: $ParseParams | undefined): $ParseParams {
10+
return params?.Err ? { callee: params.callee ?? callee, Err: params.Err } : { callee: params?.callee ?? callee };
11+
}
712

813
/////////// METHODS ///////////
914
export type $Parse = <T extends schemas.$ZodType>(
1015
schema: T,
1116
value: unknown,
1217
_ctx?: schemas.ParseContext<errors.$ZodIssue>,
13-
_params?: { callee?: util.AnyFunc; Err?: $ZodErrorClass }
18+
_params?: $ParseParams
1419
) => core.output<T>;
1520

16-
export const _parse: (_Err: $ZodErrorClass) => $Parse = (_Err) => (schema, value, _ctx, _params) => {
17-
const ctx: schemas.ParseContextInternal = _ctx ? { ..._ctx, async: false } : { async: false };
18-
const result = schema._zod.run({ value, issues: [] }, ctx);
19-
if (result instanceof Promise) {
20-
throw new core.$ZodAsyncError();
21-
}
22-
if (result.issues.length) {
23-
const e = new (_params?.Err ?? _Err)(result.issues.map((iss) => util.finalizeIssue(iss, ctx, core.config())));
24-
util.captureStackTrace(e, _params?.callee);
25-
throw e;
26-
}
27-
return result.value as core.output<typeof schema>;
21+
export const _parse: (_Err: $ZodErrorClass) => $Parse = (_Err) => {
22+
const fn: $Parse = (schema, value, _ctx, _params) => {
23+
const ctx: schemas.ParseContextInternal = _ctx ? { ..._ctx, async: false } : { async: false };
24+
const result = schema._zod.run({ value, issues: [] }, ctx);
25+
if (result instanceof Promise) {
26+
throw new core.$ZodAsyncError();
27+
}
28+
if (result.issues.length) {
29+
const e = new (_params?.Err ?? _Err)(result.issues.map((iss) => util.finalizeIssue(iss, ctx, core.config())));
30+
util.captureStackTrace(e, _params?.callee ?? fn);
31+
throw e;
32+
}
33+
return result.value as core.output<typeof schema>;
34+
};
35+
return fn;
2836
};
2937

3038
export const parse: $Parse = /* @__PURE__*/ _parse(errors.$ZodRealError);
@@ -33,19 +41,22 @@ export type $ParseAsync = <T extends schemas.$ZodType>(
3341
schema: T,
3442
value: unknown,
3543
_ctx?: schemas.ParseContext<errors.$ZodIssue>,
36-
_params?: { callee?: util.AnyFunc; Err?: $ZodErrorClass }
44+
_params?: $ParseParams
3745
) => Promise<core.output<T>>;
3846

39-
export const _parseAsync: (_Err: $ZodErrorClass) => $ParseAsync = (_Err) => async (schema, value, _ctx, params) => {
40-
const ctx: schemas.ParseContextInternal = _ctx ? { ..._ctx, async: true } : { async: true };
41-
let result = schema._zod.run({ value, issues: [] }, ctx);
42-
if (result instanceof Promise) result = await result;
43-
if (result.issues.length) {
44-
const e = new (params?.Err ?? _Err)(result.issues.map((iss) => util.finalizeIssue(iss, ctx, core.config())));
45-
util.captureStackTrace(e, params?.callee);
46-
throw e;
47-
}
48-
return result.value as core.output<typeof schema>;
47+
export const _parseAsync: (_Err: $ZodErrorClass) => $ParseAsync = (_Err) => {
48+
const fn: $ParseAsync = async (schema, value, _ctx, params) => {
49+
const ctx: schemas.ParseContextInternal = _ctx ? { ..._ctx, async: true } : { async: true };
50+
let result = schema._zod.run({ value, issues: [] }, ctx);
51+
if (result instanceof Promise) result = await result;
52+
if (result.issues.length) {
53+
const e = new (params?.Err ?? _Err)(result.issues.map((iss) => util.finalizeIssue(iss, ctx, core.config())));
54+
util.captureStackTrace(e, params?.callee ?? fn);
55+
throw e;
56+
}
57+
return result.value as core.output<typeof schema>;
58+
};
59+
return fn;
4960
};
5061

5162
export const parseAsync: $ParseAsync = /* @__PURE__*/ _parseAsync(errors.$ZodRealError);
@@ -97,49 +108,69 @@ export const safeParseAsync: $SafeParseAsync = /* @__PURE__*/ _safeParseAsync(er
97108
export type $Encode = <T extends schemas.$ZodType>(
98109
schema: T,
99110
value: core.output<T>,
100-
_ctx?: schemas.ParseContext<errors.$ZodIssue>
111+
_ctx?: schemas.ParseContext<errors.$ZodIssue>,
112+
_params?: $ParseParams
101113
) => core.input<T>;
102114

103-
export const _encode: (_Err: $ZodErrorClass) => $Encode = (_Err) => (schema, value, _ctx) => {
104-
const ctx = _ctx ? { ..._ctx, direction: "backward" as const } : { direction: "backward" as const };
105-
return _parse(_Err)(schema, value, ctx as any) as any;
115+
export const _encode: (_Err: $ZodErrorClass) => $Encode = (_Err) => {
116+
const parse = _parse(_Err);
117+
const fn: $Encode = (schema, value, _ctx, _params) => {
118+
const ctx = _ctx ? { ..._ctx, direction: "backward" as const } : { direction: "backward" as const };
119+
return parse(schema, value, ctx as any, finalizeParams(fn, _params)) as any;
120+
};
121+
return fn;
106122
};
107123

108124
export const encode: $Encode = /* @__PURE__*/ _encode(errors.$ZodRealError);
109125

110126
export type $Decode = <T extends schemas.$ZodType>(
111127
schema: T,
112128
value: core.input<T>,
113-
_ctx?: schemas.ParseContext<errors.$ZodIssue>
129+
_ctx?: schemas.ParseContext<errors.$ZodIssue>,
130+
_params?: $ParseParams
114131
) => core.output<T>;
115132

116-
export const _decode: (_Err: $ZodErrorClass) => $Decode = (_Err) => (schema, value, _ctx) => {
117-
return _parse(_Err)(schema, value, _ctx);
133+
export const _decode: (_Err: $ZodErrorClass) => $Decode = (_Err) => {
134+
const parse = _parse(_Err);
135+
const fn: $Decode = (schema, value, _ctx, _params) => {
136+
return parse(schema, value, _ctx, finalizeParams(fn, _params));
137+
};
138+
return fn;
118139
};
119140

120141
export const decode: $Decode = /* @__PURE__*/ _decode(errors.$ZodRealError);
121142

122143
export type $EncodeAsync = <T extends schemas.$ZodType>(
123144
schema: T,
124145
value: core.output<T>,
125-
_ctx?: schemas.ParseContext<errors.$ZodIssue>
146+
_ctx?: schemas.ParseContext<errors.$ZodIssue>,
147+
_params?: $ParseParams
126148
) => Promise<core.input<T>>;
127149

128-
export const _encodeAsync: (_Err: $ZodErrorClass) => $EncodeAsync = (_Err) => async (schema, value, _ctx) => {
129-
const ctx = _ctx ? { ..._ctx, direction: "backward" as const } : { direction: "backward" as const };
130-
return _parseAsync(_Err)(schema, value, ctx as any) as any;
150+
export const _encodeAsync: (_Err: $ZodErrorClass) => $EncodeAsync = (_Err) => {
151+
const parseAsync = _parseAsync(_Err);
152+
const fn: $EncodeAsync = async (schema, value, _ctx, _params) => {
153+
const ctx = _ctx ? { ..._ctx, direction: "backward" as const } : { direction: "backward" as const };
154+
return parseAsync(schema, value, ctx as any, finalizeParams(fn, _params)) as any;
155+
};
156+
return fn;
131157
};
132158

133159
export const encodeAsync: $EncodeAsync = /* @__PURE__*/ _encodeAsync(errors.$ZodRealError);
134160

135161
export type $DecodeAsync = <T extends schemas.$ZodType>(
136162
schema: T,
137163
value: core.input<T>,
138-
_ctx?: schemas.ParseContext<errors.$ZodIssue>
164+
_ctx?: schemas.ParseContext<errors.$ZodIssue>,
165+
_params?: $ParseParams
139166
) => Promise<core.output<T>>;
140167

141-
export const _decodeAsync: (_Err: $ZodErrorClass) => $DecodeAsync = (_Err) => async (schema, value, _ctx) => {
142-
return _parseAsync(_Err)(schema, value, _ctx);
168+
export const _decodeAsync: (_Err: $ZodErrorClass) => $DecodeAsync = (_Err) => {
169+
const parseAsync = _parseAsync(_Err);
170+
const fn: $DecodeAsync = async (schema, value, _ctx, _params) => {
171+
return parseAsync(schema, value, _ctx, finalizeParams(fn, _params));
172+
};
173+
return fn;
143174
};
144175

145176
export const decodeAsync: $DecodeAsync = /* @__PURE__*/ _decodeAsync(errors.$ZodRealError);

0 commit comments

Comments
 (0)