Skip to content

Async field-level validation race causing suppressed errors and incorrect validating flags #509

Description

@kallal79

Summary

  1. When multiple fields have independent async field-level validators (validateFields: []), making rapid changes to different fields can cause some fields' validation results to be suppressed (not applied to their FieldState) — issue reproducible and reported by When using async field-level validation with validateFields set to [], making changes to multiple fields causes some errors to be suppressed. #492.

  2. When a field runs multiple overlapping async validations (user edits while a previous validation is still pending), the validating flag on that FieldState may be reset to false when an earlier promise resolves while a later validation is still running — reported by When using async field-level validation, the validating FieldState gets reset to false even if validation is ongoing #491.

Why this is high priority

  • Validation correctness is fundamental. If users see inconsistent errors or validations are silently dropped, this can lead to incorrect form acceptance or incorrect UX and bugs in apps using Final Form.
  • Both issues manifest with async validators (very common for server-side uniqueness checks, availability checks, etc.).

Reproduction (POC)

I added a reproducible POC script at poc/async-validation-poc.ts. Two scenarios are provided:

  1. scenario1_suppressedErrors demonstrates that two fields with validateFields: [] and independent async validators may end with one field's error missing.
  2. scenario2_validatingFlag demonstrates overlapping validations on the same field where validating can be inconsistent.

Run the POC locally (recommended to use npx ts-node):

# from repository root
npx ts-node poc/async-validation-poc.ts

  • Each field's async validation should set the correct error state when its own validator finishes, regardless of concurrent validations on other fields.
  • If multiple async validators run for the same field, validating should remain true until all outstanding validations for that field have finished, and only then flip to false.

Observed behavior

  • Some validations' error values are not persisted/published after concurrent validations (suppression).
  • validating toggles too early in some workflows (flips to false while newer validations still in-flight), or subscriber notifications are missed or duplicated due to race conditions.

Root cause analysis (summary)

  • Historically, Final Form kept a single nextAsyncValidationKey for global async runs and used Promise resolution order + comparison with that key to decide whether to ignore a resolved result.
  • Field-level async validations were not tracked per-field sufficiently (no per-field counters/keys), so later runs could cause earlier ones to override or be ignored incorrectly.
  • The notification contract (field subscribers vs global form subscribers) expects precise ordering and counts of notifications; adding per-field merging without carefully scheduling notifications can cause duplicate or missing notifications or re-entrancy into the validation loop.

Suggested fixes

  1. Track per-field validating counters (increment when a field's async validator starts, decrement when it resolves). Publish a boolean validating to consumers (true if counter > 0). This prevents early flipping to false while later validations are still running.
  2. Record per-field "last async run id" along with a global run id to determine relevance of a resolved async result and avoid suppressing applicable results.
  3. Merge field-level async results into a per-run temporary store and merge into formState.errors at resolution-time for relevant fields (only apply per-field results that have not been superseded).
  4. Schedule notifications in a way that avoids re-entrancy into validation (use the configured callback scheduler for async notifications) and ensure we don't double-notify subscribers while still ensuring missing notifications are delivered.

Files added as part of the investigation

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