Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions packages/zod/src/v4/classic/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,25 +36,29 @@ export const safeParseAsync: <T extends core.$ZodType>(
export const encode: <T extends core.$ZodType>(
schema: T,
value: core.output<T>,
_ctx?: core.ParseContext<core.$ZodIssue>
_ctx?: core.ParseContext<core.$ZodIssue>,
_params?: { callee?: core.util.AnyFunc; Err?: core.$ZodErrorClass }
) => core.input<T> = /* @__PURE__ */ core._encode(ZodRealError);

export const decode: <T extends core.$ZodType>(
schema: T,
value: core.input<T>,
_ctx?: core.ParseContext<core.$ZodIssue>
_ctx?: core.ParseContext<core.$ZodIssue>,
_params?: { callee?: core.util.AnyFunc; Err?: core.$ZodErrorClass }
) => core.output<T> = /* @__PURE__ */ core._decode(ZodRealError);

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

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

export const safeEncode: <T extends core.$ZodType>(
Expand Down
8 changes: 4 additions & 4 deletions packages/zod/src/v4/classic/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,10 +237,10 @@ export const ZodType: core.$constructor<ZodType> = /*@__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);
Expand Down
68 changes: 68 additions & 0 deletions packages/zod/src/v4/classic/tests/error.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<unknown>): Promise<unknown> {
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([]);

Expand Down Expand Up @@ -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);
Expand Down
107 changes: 69 additions & 38 deletions packages/zod/src/v4/core/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <T extends schemas.$ZodType>(
schema: T,
value: unknown,
_ctx?: schemas.ParseContext<errors.$ZodIssue>,
_params?: { callee?: util.AnyFunc; Err?: $ZodErrorClass }
_params?: $ParseParams
) => core.output<T>;

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<typeof schema>;
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<typeof schema>;
};
return fn;
};

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

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<typeof schema>;
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<typeof schema>;
};
return fn;
};

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

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);

export type $Decode = <T extends schemas.$ZodType>(
schema: T,
value: core.input<T>,
_ctx?: schemas.ParseContext<errors.$ZodIssue>
_ctx?: schemas.ParseContext<errors.$ZodIssue>,
_params?: $ParseParams
) => core.output<T>;

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);

export type $EncodeAsync = <T extends schemas.$ZodType>(
schema: T,
value: core.output<T>,
_ctx?: schemas.ParseContext<errors.$ZodIssue>
_ctx?: schemas.ParseContext<errors.$ZodIssue>,
_params?: $ParseParams
) => Promise<core.input<T>>;

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);

export type $DecodeAsync = <T extends schemas.$ZodType>(
schema: T,
value: core.input<T>,
_ctx?: schemas.ParseContext<errors.$ZodIssue>
_ctx?: schemas.ParseContext<errors.$ZodIssue>,
_params?: $ParseParams
) => Promise<core.output<T>>;

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);
Expand Down
Loading