Skip to content

fix: stop async object validation early when abortEarly is enabled#1417

Open
yslpn wants to merge 3 commits intoopen-circle:mainfrom
yslpn:fix/async-object-abort-early
Open

fix: stop async object validation early when abortEarly is enabled#1417
yslpn wants to merge 3 commits intoopen-circle:mainfrom
yslpn:fix/async-object-abort-early

Conversation

@yslpn
Copy link
Contributor

@yslpn yslpn commented Mar 8, 2026

PR Description

This PR improves abortEarly behavior for asynchronous object schemas.

Previously, objectAsync, strictObjectAsync, looseObjectAsync, and objectWithRestAsync started validating all async entries in parallel before stopping on the first issue. That meant abortEarly: true still paid the cost of starting later async validators.

With this change:

  • async object schemas still validate entries in parallel by default
  • async object schemas switch to sequential validation when abortEarly: true
  • later async validators are no longer started after the first early-abort condition is reached

Summary by CodeRabbit

  • Bug Fixes

    • Async schema validation now reliably stops after the first error when abortEarly is enabled, preventing extra validations.
  • Tests

    • Added end-to-end tests covering early-abort behavior for async validation across schema variants.
  • Refactor

    • Internal validation flow reorganized to support sequential early-abort processing without changing public APIs.

Copilot AI review requested due to automatic review settings March 8, 2026 19:59
@vercel
Copy link

vercel bot commented Mar 8, 2026

@yslpn is attempting to deploy a commit to the Open Circle Team on Vercel.

A member of the Team first needs to authorize it.

@dosubot dosubot bot added the size:XL This PR changes 500-999 lines, ignoring generated files. label Mar 8, 2026
@coderabbitai
Copy link

coderabbitai bot commented Mar 8, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0accd66c-1f03-4dc4-94c4-e312095cb39c

📥 Commits

Reviewing files that changed from the base of the PR and between 303f27d and 5c4fa27.

📒 Files selected for processing (1)
  • library/src/schemas/objectWithRest/objectWithRestAsync.ts

📝 Walkthrough

Walkthrough

Refactors async object schema parsers (object, looseObject, objectWithRest, strictObject) to split per-entry work into parseEntry and processEntry helpers, enabling sequential validation with early abort when abortEarly is true while preserving parallel behavior when false; adds tests that assert abortEarly sequential behavior.

Changes

Cohort / File(s) Summary
Schema Tests (added sequential-abort tests)
library/src/schemas/object/objectAsync.test.ts, library/src/schemas/looseObject/looseObjectAsync.test.ts, library/src/schemas/strictObject/strictObjectAsync.test.ts, library/src/schemas/objectWithRest/objectWithRestAsync.test.ts
Imported checkAsync and pipeAsync; added "for sequential async validation with abort early" tests that record validation call order and assert only the first failing async validator runs when abortEarly: true.
Object schema implementations (refactor to parse/process)
library/src/schemas/object/objectAsync.ts, library/src/schemas/looseObject/looseObjectAsync.ts, library/src/schemas/strictObject/strictObjectAsync.ts, library/src/schemas/objectWithRest/objectWithRestAsync.ts
Replaced inline Promise.all per-entry flow with parseEntry and processEntry helpers. Implemented conditional control flow: sequential per-entry processing with early-exit signaling when abortEarly is true; preserved parallel parsing/processing when false. Adjusted value handling, dataset population, and issue propagation accordingly.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Poem

🐰 I hopped through entries, one by one,
parsing, processing ’til the work was done.
If early abort whispers "stop, don't race,"
I pause the rest and save some pace.
A tiny hop for cleaner flow — hooray! 🥕

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately describes the main change: implementing early stopping for async object validation when abortEarly is enabled across multiple async object schemas.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@dosubot dosubot bot added the fix A smaller enhancement or bug fix label Mar 8, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +359 to +363
// Process each rest entry of schema if necessary
if (!dataset.issues || !config.abortEarly) {
for (const restDataset of restDatasets) {
processRestEntry(restDataset);
}
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

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

In the else branch (which is only entered when config.abortEarly is falsy), the condition !dataset.issues || !config.abortEarly on line 360 is always true because config.abortEarly is guaranteed to be falsy here. The !config.abortEarly part is a dead/redundant condition that can be simplified to just !dataset.issues or removed entirely (always processing rest entries in the non-abort-early path).

Suggested change
// Process each rest entry of schema if necessary
if (!dataset.issues || !config.abortEarly) {
for (const restDataset of restDatasets) {
processRestEntry(restDataset);
}
// Process each rest entry of schema
for (const restDataset of restDatasets) {
processRestEntry(restDataset);

Copilot uses AI. Check for mistakes.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
library/src/schemas/object/objectAsync.ts (1)

233-248: Consider extracting shared helpers in a future refactor.

The parseEntry and processEntry patterns are nearly identical across objectAsync, looseObjectAsync, strictObjectAsync, and objectWithRestAsync. While the current inline approach works correctly, extracting these into shared internal utilities (e.g., _parseObjectEntry, _processObjectEntry) could reduce duplication.

This is not blocking for the current PR as the implementation is correct.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@library/src/schemas/object/objectAsync.ts` around lines 233 - 248, The code
in objectAsync repeats inline parseEntry and processEntry logic also used in
looseObjectAsync, strictObjectAsync, and objectWithRestAsync; extract these into
shared internal helpers (e.g., _parseObjectEntry and _processObjectEntry) and
replace the inline closures in each file with calls to those helpers to remove
duplication while preserving behavior (keep async semantics and abortEarly
branching in the callers like objectAsync). Ensure the new helpers accept the
same inputs previously captured by the closures (entry tuple or parsed value and
the config/context needed), return the same outputs/promises, and update all
references in objectAsync, looseObjectAsync, strictObjectAsync, and
objectWithRestAsync to call the helpers.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@library/src/schemas/object/objectAsync.ts`:
- Around line 233-248: The code in objectAsync repeats inline parseEntry and
processEntry logic also used in looseObjectAsync, strictObjectAsync, and
objectWithRestAsync; extract these into shared internal helpers (e.g.,
_parseObjectEntry and _processObjectEntry) and replace the inline closures in
each file with calls to those helpers to remove duplication while preserving
behavior (keep async semantics and abortEarly branching in the callers like
objectAsync). Ensure the new helpers accept the same inputs previously captured
by the closures (entry tuple or parsed value and the config/context needed),
return the same outputs/promises, and update all references in objectAsync,
looseObjectAsync, strictObjectAsync, and objectWithRestAsync to call the
helpers.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 71a6a518-1de8-456a-be99-6f872d86d693

📥 Commits

Reviewing files that changed from the base of the PR and between 97962a6 and 303f27d.

📒 Files selected for processing (8)
  • library/src/schemas/looseObject/looseObjectAsync.test.ts
  • library/src/schemas/looseObject/looseObjectAsync.ts
  • library/src/schemas/object/objectAsync.test.ts
  • library/src/schemas/object/objectAsync.ts
  • library/src/schemas/objectWithRest/objectWithRestAsync.test.ts
  • library/src/schemas/objectWithRest/objectWithRestAsync.ts
  • library/src/schemas/strictObject/strictObjectAsync.test.ts
  • library/src/schemas/strictObject/strictObjectAsync.ts

@fabian-hiller
Copy link
Member

Thank you for creating this PR. Is there any benefits of integrating and providing an AbortController instance for async schema validation. I would like to generally find a better implementation for Valibot v2 or v3 for async. We should already have issues and maybe PRs about this topic. @dosu can you summarize them?

@fabian-hiller fabian-hiller self-assigned this Mar 8, 2026
@fabian-hiller fabian-hiller added the next version Something to look at for our next major release label Mar 8, 2026
@dosubot
Copy link

dosubot bot commented Mar 8, 2026

Yes, there are existing issues and PRs about async validation improvements. Here's a summary:

AbortController Support
PR #1203 adds a signal parameter to parseAsync that propagates to async validators. This enables canceling in-flight operations (e.g., aborting fetch() calls mid-execution). Aborted validations throw an AbortError. The PR is open and awaiting merge.

Early Abort Optimization (this PR)
Issue #954 documents that abortEarly: true currently awaits all parallel async promises before checking the flag, defeating its purpose. Your PR (#1417) fixes this by switching to sequential validation when abortEarly is enabled. Maintainers have confirmed this is on the v2 roadmap.

These two are complementary:

Other Related Discussions

  • Issue #923: memoize/memoizeAsync for caching async validation results (planned post-v1, potentially alongside debounce)
  • Discussion #267: Adding a promise() schema to handle Promise values directly

Currently, the codebase has no AbortController support — PR #1203 would add it. Together with this PR, they'd provide comprehensive async validation control.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other

Chat with Dosu Open in Cursor Join Discord Share on X

@yslpn
Copy link
Contributor Author

yslpn commented Mar 10, 2026

Thank you for creating this PR. Is there any benefits of integrating and providing an AbortController instance for async schema validation. I would like to generally find a better implementation for Valibot v2 or v3 for async. We should already have issues and maybe PRs about this topic. @dosu can you summarize them?

Yes, I think AbortController would be beneficial, but it addresses a different concern.

This PR makes abortEarly: true stop starting new async validations. AbortController would additionally allow canceling async work that has already started. So I see them as complementary rather than competing approaches.

That also makes me think this PR is still valuable on its own for v1, while broader cancellation support can be explored separately for v2

@dosubot
Copy link

dosubot bot commented Mar 10, 2026

Exactly right — your PR prevents starting unnecessary work, while AbortController would cancel work already in progress. They solve different parts of the problem and would work well together.

For reference, PR #1203 already implements AbortController support by adding a signal parameter to parseAsync that propagates to async validators. That could be a good foundation for the v2/v3 cancellation work you mentioned.

To reply, just mention @dosu.


How did I do? Good | Irrelevant | Incorrect | Verbose | Hallucination | Report 🐛 | Other

Chat with Dosu Open in Cursor Join Discord Share on X

@fabian-hiller
Copy link
Member

If someone sets abortEarly to true for a schema with multiple async validations, would this PR massively slow down the validation in the success cases?

@yslpn
Copy link
Contributor Author

yslpn commented Mar 11, 2026

If someone sets abortEarly to true for a schema with multiple async validations, would this PR massively slow down the validation in the success cases?

That's true.

But we need to fix this. There are only two options:

  1. Remove abortEarly support for asynchronous checks. It doesn't work right now.
  2. Use the current version, which consistently validates but slows down validation for all valid cases.

The first option can be considered a breaking change.

Which option do you prefer?

@fabian-hiller
Copy link
Member

Wouldn't it be best to validate in parallel but abort via AbortSignal on the first error we receive?

@yslpn
Copy link
Contributor Author

yslpn commented Mar 14, 2026

Wouldn't it be best to validate in parallel but abort via AbortSignal on the first error we receive?

That would be great, but here's what I'm thinking.

AbortSignal only works where the specific API supports it. It can't cancel "any already running Promise." If async-check simply awaits someSdkCall() and the SDK doesn't know about signal, it can't be aborted from the outside.

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

Labels

fix A smaller enhancement or bug fix next version Something to look at for our next major release size:XL This PR changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants