Skip to content

fix: respect abort: true in .refine() for checks with when function#5681

Merged
colinhacks merged 1 commit into
mainfrom
pullfrog/5680-fix-abort-true-in-refine
Feb 8, 2026
Merged

fix: respect abort: true in .refine() for checks with when function#5681
colinhacks merged 1 commit into
mainfrom
pullfrog/5680-fix-abort-true-in-refine

Conversation

@pullfrog
Copy link
Copy Markdown
Contributor

@pullfrog pullfrog Bot commented Feb 1, 2026

Summary

Fixes the issue where abort: true in .refine() was not stopping subsequent checks like .min().

Problem

const schema = z.object({
  name: z
    .string()
    .refine((v) => v.length > 0, {
      abort: true,
      message: "Required",
    })
    .refine(() => false, { message: "Error" })
    .min(3),
});

schema.safeParse({ name: "" });
// Before fix: 2 issues (Required + too_small)
// After fix: 1 issue (Required)

Root Cause

Built-in checks like .min() have a when function (used to skip checks on null/undefined values). When a check had a when function, the abort check was bypassed entirely, meaning abort: true only stopped subsequent checks without a when function.

Solution

Added explicitlyAborted() to distinguish between:

  • Explicit abort (continue: false) — from abort: true in .refine()
  • Implicit abort (continue: undefined) — from type errors like invalid_type

Checks with a when function now respect explicit aborts while still allowing implicit aborts to pass through. This preserves the existing behavior where type errors don't prevent other checks from running (e.g., array element type errors don't prevent min(2) from running).

Test Results

All 3573 tests pass.

Closes #5680

Pullfrog  | Triggered by Pullfrog | Using Claude CodeView workflow runpullfrog.com𝕏

…tion

The `abort: true` option in `.refine()` was not stopping subsequent checks
like `.min()` because those checks have a `when` function that bypassed
the abort check entirely.

This fix adds `explicitlyAborted()` to distinguish between:
- Explicit abort (`continue: false`) from `abort: true` in `.refine()`
- Implicit abort (`continue: undefined`) from type errors

Checks with a `when` function now respect explicit aborts while still
allowing implicit aborts to pass through, preserving existing behavior
where type errors don't prevent other checks from running.

Closes #5680
@dosubot
Copy link
Copy Markdown

dosubot Bot commented Feb 1, 2026

Related Documentation

No published documentation to review for changes on this repository.

Write your first living document

How did I do? Any feedback?  Join Discord

@colinhacks
Copy link
Copy Markdown
Owner

cc @pullfrog review

Copy link
Copy Markdown
Contributor Author

@pullfrog pullfrog Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low urgency. The core logic fix is correct and addresses the reported issue. The play.ts file contains unrelated debugging code that should be reverted to its original state before merging.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow runpullfrog.com𝕏

Comment thread play.ts Outdated
Comment on lines +1 to +23
let x: "a" | "b" | false = false as "a" | "b" | false;

const type1 = z.literal(["a", "b", "c"]);
const type2 = z.literal([1, 2, 3]);
for (const item of [1, 2, 3]) {
if (x === "a") {
// branch 1: doesn't always continue
console.log("a");
} else if (x) {
// branch 2: continue narrows x to false after this
continue;
}

const record = z.partialRecord(z.union([type1, type2]), z.string());
// TS thinks x is false | "a" here (eliminated "b" via the continue)
// But actually x could be "a" | "b" | false because:
// - branch 1 falls through without continue when x === "a"
// - branch 2 continues when x === "b"
// - x === false falls through
// After mutation below, x could be any value on next iteration

console.log(record.parse({ a: "1", 2: "4" }));
// This comparison errors: TS says x can't be "b"
if (x !== "b") {
x = "b"; // but we can assign "b" here
}
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file was modified with unrelated TypeScript flow control analysis code. It should be reverted to its original content:

import { z } from "zod";

const type1 = z.literal(["a", "b", "c"]);
const type2 = z.literal([1, 2, 3]);

const record = z.partialRecord(z.union([type1, type2]), z.string());

console.log(record.parse({ a: "1", 2: "4" }));

Comment thread packages/zod/src/v4/core/util.ts Outdated
Comment on lines +803 to +804
// Returns "explicit" for hard aborts (continue: false, from abort:true),
// "implicit" for soft aborts (continue: undefined, from type errors),
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor documentation nit: The comment says continue: undefined is "from type errors", but looking at the code, type errors actually set continue: false (e.g., in checks.ts line 300). The continue: undefined case is from built-in checks like .min(), .max(), etc. Consider updating to:

// Returns "explicit" for hard aborts (continue: false, from abort:true or type errors),
// "implicit" for soft aborts (continue: undefined, from built-in checks),

@colinhacks colinhacks force-pushed the pullfrog/5680-fix-abort-true-in-refine branch from 154665c to 32e8333 Compare February 8, 2026 18:13
@colinhacks colinhacks merged commit 5b57450 into main Feb 8, 2026
10 checks passed
@colinhacks colinhacks deleted the pullfrog/5680-fix-abort-true-in-refine branch February 8, 2026 18:14
@colinhacks
Copy link
Copy Markdown
Owner

Landed in Zod 4.4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

abort: true in refine does not stop further validations

1 participant