diff --git a/packages/zod/src/v4/classic/parse.ts b/packages/zod/src/v4/classic/parse.ts index 7df7b290da..fdbc71d7e1 100644 --- a/packages/zod/src/v4/classic/parse.ts +++ b/packages/zod/src/v4/classic/parse.ts @@ -36,25 +36,29 @@ export const safeParseAsync: ( export const encode: ( schema: T, value: core.output, - _ctx?: core.ParseContext + _ctx?: core.ParseContext, + _params?: { callee?: core.util.AnyFunc; Err?: core.$ZodErrorClass } ) => core.input = /* @__PURE__ */ core._encode(ZodRealError); export const decode: ( schema: T, value: core.input, - _ctx?: core.ParseContext + _ctx?: core.ParseContext, + _params?: { callee?: core.util.AnyFunc; Err?: core.$ZodErrorClass } ) => core.output = /* @__PURE__ */ core._decode(ZodRealError); export const encodeAsync: ( schema: T, value: core.output, - _ctx?: core.ParseContext + _ctx?: core.ParseContext, + _params?: { callee?: core.util.AnyFunc; Err?: core.$ZodErrorClass } ) => Promise> = /* @__PURE__ */ core._encodeAsync(ZodRealError); export const decodeAsync: ( schema: T, value: core.input, - _ctx?: core.ParseContext + _ctx?: core.ParseContext, + _params?: { callee?: core.util.AnyFunc; Err?: core.$ZodErrorClass } ) => Promise> = /* @__PURE__ */ core._decodeAsync(ZodRealError); export const safeEncode: ( diff --git a/packages/zod/src/v4/classic/schemas.ts b/packages/zod/src/v4/classic/schemas.ts index 64aceb4f95..a885498891 100644 --- a/packages/zod/src/v4/classic/schemas.ts +++ b/packages/zod/src/v4/classic/schemas.ts @@ -237,10 +237,10 @@ export const ZodType: core.$constructor = /*@__PURE__*/ core.$construct inst.parseAsync = async (data, params) => parse.parseAsync(inst, data, params, { callee: inst.parseAsync }); inst.safeParseAsync = async (data, params) => parse.safeParseAsync(inst, data, params); inst.spa = inst.safeParseAsync; - inst.encode = (data, params) => parse.encode(inst, data, params); - inst.decode = (data, params) => parse.decode(inst, data, params); - inst.encodeAsync = async (data, params) => parse.encodeAsync(inst, data, params); - inst.decodeAsync = async (data, params) => parse.decodeAsync(inst, data, params); + inst.encode = (data, params) => parse.encode(inst, data, params, { callee: inst.encode }); + inst.decode = (data, params) => parse.decode(inst, data, params, { callee: inst.decode }); + inst.encodeAsync = async (data, params) => parse.encodeAsync(inst, data, params, { callee: inst.encodeAsync }); + inst.decodeAsync = async (data, params) => parse.decodeAsync(inst, data, params, { callee: inst.decodeAsync }); inst.safeEncode = (data, params) => parse.safeEncode(inst, data, params); inst.safeDecode = (data, params) => parse.safeDecode(inst, data, params); inst.safeEncodeAsync = async (data, params) => parse.safeEncodeAsync(inst, data, params); diff --git a/packages/zod/src/v4/classic/tests/error.test.ts b/packages/zod/src/v4/classic/tests/error.test.ts index d884b7e46b..cf73c793f1 100644 --- a/packages/zod/src/v4/classic/tests/error.test.ts +++ b/packages/zod/src/v4/classic/tests/error.test.ts @@ -2,6 +2,36 @@ import { inspect } from "node:util"; import { expect, test } from "vitest"; import * as z from "zod/v4"; +function getThrownError(fn: () => unknown): unknown { + try { + fn(); + } catch (error) { + return error; + } + throw new Error("Expected function to throw"); +} + +async function getRejectedError(fn: () => Promise): Promise { + try { + await fn(); + } catch (error) { + return error; + } + throw new Error("Expected function to reject"); +} + +function expectFirstStackFrameAtCallsite(error: unknown): void { + expect(error).toBeInstanceOf(Error); + const stack = (error as Error).stack; + expect(stack).toEqual(expect.any(String)); + + const firstFrame = stack!.split("\n").find((line) => line.trim().startsWith("at ")); + expect(firstFrame).toBeDefined(); + expect(firstFrame).toContain("error.test.ts"); + expect(firstFrame).not.toContain("core/parse.ts"); + expect(firstFrame).not.toContain("classic/schemas.ts"); +} + test("error creation", () => { const err1 = new z.ZodError([]); @@ -683,6 +713,44 @@ test("error inheritance", () => { } }); +test("parse errors capture the caller stack frame", async () => { + const schema = z.string(); + const parse = schema.parse; + const parseAsync = schema.parseAsync; + + expectFirstStackFrameAtCallsite(getThrownError(() => schema.parse(123))); + expectFirstStackFrameAtCallsite(getThrownError(() => parse(123))); + expectFirstStackFrameAtCallsite(getThrownError(() => z.parse(schema, 123))); + + expectFirstStackFrameAtCallsite(await getRejectedError(() => schema.parseAsync(123))); + expectFirstStackFrameAtCallsite(await getRejectedError(() => parseAsync(123))); + expectFirstStackFrameAtCallsite(await getRejectedError(() => z.parseAsync(schema, 123))); +}); + +test("codec errors capture the caller stack frame", async () => { + const schema = z.string(); + const encode = schema.encode; + const decode = schema.decode; + const encodeAsync = schema.encodeAsync; + const decodeAsync = schema.decodeAsync; + + expectFirstStackFrameAtCallsite(getThrownError(() => schema.encode(123 as any))); + expectFirstStackFrameAtCallsite(getThrownError(() => encode(123 as any))); + expectFirstStackFrameAtCallsite(getThrownError(() => z.encode(schema, 123 as any))); + + expectFirstStackFrameAtCallsite(getThrownError(() => schema.decode(123 as any))); + expectFirstStackFrameAtCallsite(getThrownError(() => decode(123 as any))); + expectFirstStackFrameAtCallsite(getThrownError(() => z.decode(schema, 123 as any))); + + expectFirstStackFrameAtCallsite(await getRejectedError(() => schema.encodeAsync(123 as any))); + expectFirstStackFrameAtCallsite(await getRejectedError(() => encodeAsync(123 as any))); + expectFirstStackFrameAtCallsite(await getRejectedError(() => z.encodeAsync(schema, 123 as any))); + + expectFirstStackFrameAtCallsite(await getRejectedError(() => schema.decodeAsync(123 as any))); + expectFirstStackFrameAtCallsite(await getRejectedError(() => decodeAsync(123 as any))); + expectFirstStackFrameAtCallsite(await getRejectedError(() => z.decodeAsync(schema, 123 as any))); +}); + test("error serialization", () => { try { z.string().parse(123); diff --git a/packages/zod/src/v4/core/parse.ts b/packages/zod/src/v4/core/parse.ts index aaf2929742..4881dcc0ae 100644 --- a/packages/zod/src/v4/core/parse.ts +++ b/packages/zod/src/v4/core/parse.ts @@ -4,27 +4,35 @@ import type * as schemas from "./schemas.js"; import * as util from "./util.js"; export type $ZodErrorClass = { new (issues: errors.$ZodIssue[]): errors.$ZodError }; +type $ParseParams = { callee?: util.AnyFunc; Err?: $ZodErrorClass }; + +function finalizeParams(callee: util.AnyFunc, params: $ParseParams | undefined): $ParseParams { + return params?.Err ? { callee: params.callee ?? callee, Err: params.Err } : { callee: params?.callee ?? callee }; +} /////////// METHODS /////////// export type $Parse = ( schema: T, value: unknown, _ctx?: schemas.ParseContext, - _params?: { callee?: util.AnyFunc; Err?: $ZodErrorClass } + _params?: $ParseParams ) => core.output; -export const _parse: (_Err: $ZodErrorClass) => $Parse = (_Err) => (schema, value, _ctx, _params) => { - const ctx: schemas.ParseContextInternal = _ctx ? { ..._ctx, async: false } : { async: false }; - const result = schema._zod.run({ value, issues: [] }, ctx); - if (result instanceof Promise) { - throw new core.$ZodAsyncError(); - } - if (result.issues.length) { - const e = new (_params?.Err ?? _Err)(result.issues.map((iss) => util.finalizeIssue(iss, ctx, core.config()))); - util.captureStackTrace(e, _params?.callee); - throw e; - } - return result.value as core.output; +export const _parse: (_Err: $ZodErrorClass) => $Parse = (_Err) => { + const fn: $Parse = (schema, value, _ctx, _params) => { + const ctx: schemas.ParseContextInternal = _ctx ? { ..._ctx, async: false } : { async: false }; + const result = schema._zod.run({ value, issues: [] }, ctx); + if (result instanceof Promise) { + throw new core.$ZodAsyncError(); + } + if (result.issues.length) { + const e = new (_params?.Err ?? _Err)(result.issues.map((iss) => util.finalizeIssue(iss, ctx, core.config()))); + util.captureStackTrace(e, _params?.callee ?? fn); + throw e; + } + return result.value as core.output; + }; + return fn; }; export const parse: $Parse = /* @__PURE__*/ _parse(errors.$ZodRealError); @@ -33,19 +41,22 @@ export type $ParseAsync = ( schema: T, value: unknown, _ctx?: schemas.ParseContext, - _params?: { callee?: util.AnyFunc; Err?: $ZodErrorClass } + _params?: $ParseParams ) => Promise>; -export const _parseAsync: (_Err: $ZodErrorClass) => $ParseAsync = (_Err) => async (schema, value, _ctx, params) => { - const ctx: schemas.ParseContextInternal = _ctx ? { ..._ctx, async: true } : { async: true }; - let result = schema._zod.run({ value, issues: [] }, ctx); - if (result instanceof Promise) result = await result; - if (result.issues.length) { - const e = new (params?.Err ?? _Err)(result.issues.map((iss) => util.finalizeIssue(iss, ctx, core.config()))); - util.captureStackTrace(e, params?.callee); - throw e; - } - return result.value as core.output; +export const _parseAsync: (_Err: $ZodErrorClass) => $ParseAsync = (_Err) => { + const fn: $ParseAsync = async (schema, value, _ctx, params) => { + const ctx: schemas.ParseContextInternal = _ctx ? { ..._ctx, async: true } : { async: true }; + let result = schema._zod.run({ value, issues: [] }, ctx); + if (result instanceof Promise) result = await result; + if (result.issues.length) { + const e = new (params?.Err ?? _Err)(result.issues.map((iss) => util.finalizeIssue(iss, ctx, core.config()))); + util.captureStackTrace(e, params?.callee ?? fn); + throw e; + } + return result.value as core.output; + }; + return fn; }; export const parseAsync: $ParseAsync = /* @__PURE__*/ _parseAsync(errors.$ZodRealError); @@ -97,12 +108,17 @@ export const safeParseAsync: $SafeParseAsync = /* @__PURE__*/ _safeParseAsync(er export type $Encode = ( schema: T, value: core.output, - _ctx?: schemas.ParseContext + _ctx?: schemas.ParseContext, + _params?: $ParseParams ) => core.input; -export const _encode: (_Err: $ZodErrorClass) => $Encode = (_Err) => (schema, value, _ctx) => { - const ctx = _ctx ? { ..._ctx, direction: "backward" as const } : { direction: "backward" as const }; - return _parse(_Err)(schema, value, ctx as any) as any; +export const _encode: (_Err: $ZodErrorClass) => $Encode = (_Err) => { + const parse = _parse(_Err); + const fn: $Encode = (schema, value, _ctx, _params) => { + const ctx = _ctx ? { ..._ctx, direction: "backward" as const } : { direction: "backward" as const }; + return parse(schema, value, ctx as any, finalizeParams(fn, _params)) as any; + }; + return fn; }; export const encode: $Encode = /* @__PURE__*/ _encode(errors.$ZodRealError); @@ -110,11 +126,16 @@ export const encode: $Encode = /* @__PURE__*/ _encode(errors.$ZodRealError); export type $Decode = ( schema: T, value: core.input, - _ctx?: schemas.ParseContext + _ctx?: schemas.ParseContext, + _params?: $ParseParams ) => core.output; -export const _decode: (_Err: $ZodErrorClass) => $Decode = (_Err) => (schema, value, _ctx) => { - return _parse(_Err)(schema, value, _ctx); +export const _decode: (_Err: $ZodErrorClass) => $Decode = (_Err) => { + const parse = _parse(_Err); + const fn: $Decode = (schema, value, _ctx, _params) => { + return parse(schema, value, _ctx, finalizeParams(fn, _params)); + }; + return fn; }; export const decode: $Decode = /* @__PURE__*/ _decode(errors.$ZodRealError); @@ -122,12 +143,17 @@ export const decode: $Decode = /* @__PURE__*/ _decode(errors.$ZodRealError); export type $EncodeAsync = ( schema: T, value: core.output, - _ctx?: schemas.ParseContext + _ctx?: schemas.ParseContext, + _params?: $ParseParams ) => Promise>; -export const _encodeAsync: (_Err: $ZodErrorClass) => $EncodeAsync = (_Err) => async (schema, value, _ctx) => { - const ctx = _ctx ? { ..._ctx, direction: "backward" as const } : { direction: "backward" as const }; - return _parseAsync(_Err)(schema, value, ctx as any) as any; +export const _encodeAsync: (_Err: $ZodErrorClass) => $EncodeAsync = (_Err) => { + const parseAsync = _parseAsync(_Err); + const fn: $EncodeAsync = async (schema, value, _ctx, _params) => { + const ctx = _ctx ? { ..._ctx, direction: "backward" as const } : { direction: "backward" as const }; + return parseAsync(schema, value, ctx as any, finalizeParams(fn, _params)) as any; + }; + return fn; }; export const encodeAsync: $EncodeAsync = /* @__PURE__*/ _encodeAsync(errors.$ZodRealError); @@ -135,11 +161,16 @@ export const encodeAsync: $EncodeAsync = /* @__PURE__*/ _encodeAsync(errors.$Zod export type $DecodeAsync = ( schema: T, value: core.input, - _ctx?: schemas.ParseContext + _ctx?: schemas.ParseContext, + _params?: $ParseParams ) => Promise>; -export const _decodeAsync: (_Err: $ZodErrorClass) => $DecodeAsync = (_Err) => async (schema, value, _ctx) => { - return _parseAsync(_Err)(schema, value, _ctx); +export const _decodeAsync: (_Err: $ZodErrorClass) => $DecodeAsync = (_Err) => { + const parseAsync = _parseAsync(_Err); + const fn: $DecodeAsync = async (schema, value, _ctx, _params) => { + return parseAsync(schema, value, _ctx, finalizeParams(fn, _params)); + }; + return fn; }; export const decodeAsync: $DecodeAsync = /* @__PURE__*/ _decodeAsync(errors.$ZodRealError);