Skip to content

_handleSubmit gates form-level validation behind field-level validity, causing incomplete error reports on submit #2130

@MathisChalland

Description

@MathisChalland

Describe the bug

I believe this is a bug, though I want to flag upfront that the staging in _handleSubmit seems to be intentional. Regardless I'd like to argue the current behavior still isn't right.

_handleSubmit runs validation in two stages with an early return between them:

await this.validateAllFields('submit')             // Stage 1: field-level validators
if (!this.state.isFieldsValid){ /* ... */ return } // Gate
await this.validate('submit')                      // Stage 2: form-level validators

If Stage 1 fails, users only see the errors from this stage. So any other invalid fields stay silent until these errors are fixed. This contradicts what users intuitively expect from a submit action: check everything and show all errors.

Setting canSubmitWhenInvalid: true also doesn't fix this. The flag gates whether _handleSubmit runs at all, not how validation is staged within it.


My use case:

I only use form-level validation and canSubmitWhenInvalid: true.

Form-level validators write their errors into individual fields' errorMap. But isFieldsValid reads those entries without distinguishing source. So after a failed submit, those form-level errors count as "field-level invalid" on the next attempt — the isFieldsValid gate triggers, form-level validation is skipped, and stale errors stay on screen while new ones never appear.


This mechanism appears to be the underlying cause / relevant for several issues that have been reported separately: #1874, #1663 (see TeChn4K's reproduction there), #2034

PR #2120 in response to #2034 addresses a narrower symptom and doesn't solve the core issue: In #2034 the user has an onBlur validation on a field with conditional validation logic based on another field. The reported issue can be fixed by linking the other field with onBlurListenTo or using canSubmitWhenInvalid: true. The PR does not touch the isFieldsValid gate, so it does not address the problems above.

Your minimal, reproducible example

https://codesandbox.io/p/sandbox/tanstack-form-bug-7yxscz

Steps to reproduce

  1. Enter a value for field a, leave field b empty
  2. submit → Field b shows an error since it's empty
  3. clear field a
  4. submit again (notice canSubmitWhenInvalid is set to true) → only field b shows an error even though field a is also empty

Expected behavior

On submit, both stages should run unconditionally and isValid should be evaluated once over the combined result:

await this.validateAllFields('submit')
await this.validate('submit')

if (!this.state.isValid) {
  // onSubmitInvalid with the full error picture
  return
}
// onSubmit

Why I think this is the right behavior:

  • It matches user intuition for submit. Submit is the moment users most need a complete error report. Partial reports force them to fix-submit-fix-submit until they've discovered every problem, which is worse UX than showing everything at once.
  • When canSubmitWhenInvalid is false (the default), existing field errors already prevent _handleSubmit from running, so this change should have no effect unless there are filed level validators in which case all errors would now surface on submit instead of only the field-level ones, which I'd argue is also the right behavior. It changes behavior for forms that explicitly opt into canSubmitWhenInvalid: true — where a complete error report on every submit is exactly what you'd expect.

Happy to open a PR if there's a decision on this.

How often does this bug happen?

Every time

Screenshots or Videos

No response

Platform

not relevant

TanStack Form adapter

react-form

TanStack Form version

1.28.0

TypeScript version

No response

Additional context

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions