Async Form Validation in React Is Hard — Here’s a Predictable Way to Solve It

Published: (December 31, 2025 at 08:02 AM EST)
3 min read
Source: Dev.to

Source: Dev.to

The core problem: validation is not deterministic

Most form solutions validate based on events, not state snapshots.

Example async validator

async function validateEmail(value: string) {
  return api.checkEmailAvailability(value);
}

Imagine the user types quickly:

  1. Types taken@example.com → async request A starts
  2. Changes to john@example.com → async request B starts
  3. Request A finishes after request B

The UI shows “Email already taken” (incorrect).
Older async results overwrite newer input—a classic race condition.

Problem #1: Async validation race conditions

Goal: only the latest validation result should matter.

Many form libraries:

  • Don’t track async validation runs
  • Don’t tie validation to a stable value snapshot
  • Allow stale results to win

What a correct solution must do

  • Track async validation attempts
  • Ignore stale async results
  • Always validate against a consistent snapshot of values

Without this, async validation will never be predictable.

Problem #2: Cross‑field validation is fragile

Real forms often need to validate fields together, e.g.:

  • Confirm password must match password
  • End date must be after start date
  • A field is required only if another field is enabled

Common approaches rely on watching other fields, manual re‑validation, or hidden dependencies, which introduces implicit behavior that’s hard to debug.

Explicit cross‑field validation

register("confirmPassword", {
  validate: (value, values) =>
    value !== values.password ? "Passwords do not match" : undefined,
});
  • No magic
  • No auto re‑validation
  • No hidden dependencies – just readable logic.

Problem #3: Submit behaves differently than change

“Validation works on change, but submit behaves differently.”
This happens because many libraries:

  • Only validate touched fields
  • Skip untouched dependent fields on submit

Result: surprising submit‑time errors.

Simple rule to fix it

On submit, validate all registered fields. Always. This makes submit behavior predictable and correct.

The solution: a predictable, async‑first form engine

These problems motivated the creation of Formora, a headless React form engine built around strict principles:

  • Validation timing is explicit (change | blur | submit)
  • Async validation is race‑condition safe
  • Validation always runs against value snapshots
  • Cross‑field validation is explicit
  • No automatic dependency re‑validation

A real example using Formora

Async email validation + confirm password

const form = useForm({
  initialValues: {
    email: "",
    password: "",
    confirmPassword: "",
  },
  validateOn: "change",
  asyncDebounceMs: 500,
});

{
  await new Promise((r) => setTimeout(r, 300));
  if (value.includes("taken")) return "Email already taken";
},
})}
/>

value !== values.password ? "Passwords do not match" : undefined,
})}
/>

What this gives you:

  • Async validation that never shows stale errors
  • Explicit cross‑field logic
  • Predictable submit behavior
  • Clean, debuggable code

Why other developers can benefit from Formora

Formora isn’t trying to replace every form library; it shines when you need:

  • Correct async behavior
  • Explicit validation logic
  • Type‑safe, predictable state
  • Debuggable form behavior in real applications

If your app has async validation, cross‑field rules, or complex submit logic, Formora provides a solid, predictable foundation.

Final thoughts

Forms become difficult not because they are inherently complex, but because validation correctness is often ignored. Async logic, relationships between fields, and real user behavior require predictability, not magic. Formora was built to solve these problems explicitly and reliably.

  • GitHub:
  • npm:
Back to Blog

Related posts

Read more »

SQL makes me uncomfortable.

In my working not theoretical understanding, object‑oriented programming is not merely an alternative to the traditional functional paradigm but often feels lik...

The Web Ecosystem Civil War

markdown !Javadhttps://media2.dev.to/dynamic/image/width=50,height=50,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads...

React Coding Challenge : Card Flip Game

React Card Flip Game – Code tsx import './styles.css'; import React, { useState, useEffect } from 'react'; const values = 1, 2, 3, 4, 5; type Card = { id: numb...