You submit a login form. The browser's Network tab shows two identical OTP generation requests. The first code arrives, the second overwrites it. Your user never receives the correct token. This isn't a race condition in your API. TanStack Form's internal lifecycle triggers async validation twice on first submit when you pass a Zod schema to onSubmitAsync, a behavior rooted in how the library handles schema invocation during its validation phase.
At Fridaycode, we've seen this double execution break production authentication flows, payment forms, and any critical path where async validation has side effects. The fix isn't a configuration tweak. It requires manual validation control via safeParseAsync, re-entrancy guards to prevent race conditions, and pure validation functions that decouple side effects from schema logic. This article maps the architectural root cause, provides tested mitigation strategies beyond the basic workaround, and prescribes defensive patterns for production-grade TanStack Form async validation.
When Your Form Validation Fires Twice (Zod Async Validation)
You submit a login form. The browser's Network tab shows two identical OTP generation requests. The first code arrives, but it's already invalid - the second request overwrote it. The user can't proceed. This isn't a race condition from concurrent clicks. It's a double async validation bug in TanStack Form that fires on every first submission when using Zod's async refine or superRefine validators. The duplicate execution results in visible duplicate network requests in the browser's Network tab, potentially causing unnecessary database load and API calls. In OTP authentication flows, duplicate validation execution generates multiple different codes, invalidating the first code and leaving users unable to proceed - making this a critical UX bug rather than mere inefficiency.
The root cause isn't a configuration mistake. TanStack Form's internal lifecycle triggers validation multiple times when Zod schemas are provided to submission handlers, and subsequent submission attempts after the first one run the validation only once, indicating the issue is specific to the initial submission. The framework expects validation functions to be pure and idempotent. When async refine contains side effects like HTTP calls, those effects execute twice. The workaround - manual validation with safeParseAsync, re-entrancy guards using useRef semaphores, and decoupling side effects from validation logic - requires defensive engineering patterns that go beyond basic form configuration.
Why TanStack Form OnSubmitAsync Invokes Async Schemas Twice
The double execution isn't random. TanStack Form's onSubmitAsync internally invokes the provided schema during its validation lifecycle setup, and on first submission this initialization overlaps with the actual validation call. Subsequent submissions run once, revealing a first-run initialization issue rather than a persistent bug. The timing matters: the issue only manifests on first submission when using Zod's async refine or superRefine directly.
reproduction.ts
const asyncIgnoredUsersSchemaRefine = z.object({
username: z.string().superRefine(async (val, ctx) => {
const isIgnored = await checkUser(val); // Fires twice
if (isIgnored) ctx.addIssue({ code: 'custom', message: 'User ignored' });
})
});
const form = useForm({
onSubmitAsync: async ({ value }) => {
await asyncIgnoredUsersSchemaRefine.parseAsync(value);
}
});
Manual validation via safeParseAsync eliminates the double call because it bypasses TanStack Form's automatic schema invocation entirely. Instead of letting the framework invoke your schema during lifecycle setup, you control exactly when validation runs. This architectural insight explains why configuration tweaks fail: TanStack Form supports combining synchronous and asynchronous validation, but its internal handling of async schemas creates the overlap. The workaround isn't elegant, but it's surgical.
Fix 1: Manual Validation Control With SafeParseAsync
Because TanStack Form's internal schema invocation creates the double execution, the fix requires bypassing that automatic invocation entirely. Calling schema.safeParseAsync manually inside the submit handler eliminates the duplicate network requests by giving explicit control over when validation runs. This pattern makes validation timing visible in your code rather than hidden in framework internals.
ManualValidation.tsx
const form = useForm({
defaultValues: { email: '' },
onSubmit: async ({ value }) => {
const result = await schema.safeParseAsync(value);
if (!result.success) {
// Handle validation errors
return;
}
// Proceed with validated data
await submitToAPI(result.data);
},
});
The code above demonstrates the workaround pattern: validation executes once per submit, preventing the duplicate API calls visible in the browser's Network tab. But manual control alone isn't enough for production. Validation functions must be pure - returning boolean results or error messages without triggering side effects like HTTP requests or state mutations. Move API calls to the onSubmit handler that runs after validation succeeds. This separation makes validation idempotent and safe to run multiple times.
Production-ready async validation requires layered defense. Configure asyncDebounceMs to prevent validation storms during typing - TanStack Form provides built-in debouncing to limit how often validation triggers. Debouncing doesn't fix the double execution bug, but combined with manual validation control it creates complete defensive coverage: debouncing limits trigger frequency, manual control prevents double execution when triggered, and pure functions ensure safety if triggered unexpectedly.
Race conditions remain a risk even after applying safeParseAsync. If validation triggers multiple times in quick succession - user double-clicking submit, overlapping validation phases - only one async operation should proceed. A useRef-based execution flag acts as a semaphore, preventing concurrent validation executions. This defensive pattern is essential for operations like OTP generation or payment processing where duplicate execution has cost or security implications.
Defensive Engineering for Production Forms (production Form Validation)
What does this mean for your next production form? The double async validation issue isn't a TanStack Form quirk - it's a signal that critical user flows demand defensive patterns by default. Manual validation control eliminates double execution by replacing automatic schema invocation with explicit safeParseAsync calls in your submit handler. Pure validation functions prevent side-effect duplication by moving API calls and state mutations out of validators entirely. Re-entrancy guards stop race conditions when users double-click submit or validation phases overlap. These aren't workarounds for a bug - they're architectural necessities for any form where duplicate execution has consequences: authentication flows, payment processing, rate-limited APIs, or OTP generation.
Conclusion
When you ship your next form with TanStack Form async validation, apply manual safeParseAsync control first. Then add a useRef semaphore if the validation triggers external state changes. Finally, move side effects like OTP generation outside the schema's superRefine block entirely. The double execution issue exposes a deeper truth: automatic validation in critical flows is a liability. Production forms demand explicit control, pure validation logic, and defensive guards against race conditions. TanStack Form's internal schema handling won't change overnight, but your validation architecture can harden today.
Frequently Asked Questions
Why do my TanStack Form async validators run twice on first submit?
TanStack Form's onSubmitAsync internally invokes the provided Zod schema during its validation lifecycle setup, and on first submission this initialization overlaps with the actual validation call, causing async refine/superRefine to run twice. Subsequent submissions run the validation only once, indicating a first-run lifecycle overlap rather than random behavior.
How can I stop duplicate network requests from async validators in TanStack Form?
By bypassing TanStack Form's automatic schema invocation and calling schema.safeParseAsync manually inside your submit handler, you gain explicit control over when validation runs and eliminate the double execution that produces duplicate API calls. Also move any side-effecting API calls out of validators and into the onSubmit handler after validation succeeds.
Should validation functions perform side effects like HTTP calls?
No - the framework expects validation functions to be pure and idempotent; embedding side effects like HTTP calls in async refine/superRefine causes duplicate side effects when validation runs twice. Best practice is to keep validators pure and perform API requests only after validation succeeds in the submit handler.
Does configuring asyncDebounceMs fix the double-validation bug?
No - asyncDebounceMs can limit how often validation triggers during typing but it does not fix the first-submit double invocation caused by the framework's internal lifecycle. Debouncing is useful as part of layered defenses but must be combined with manual validation control and pure validators to fully harden critical flows.
How do I prevent race conditions if validation still overlaps or users double-click submit?
Use a useRef-based execution flag (a semaphore/re-entrancy guard) to prevent concurrent validation executions so only one async operation proceeds at a time. This defensive pattern is essential for critical operations like OTP generation or payments where duplicate execution has real cost or security implications.
Is this double-execution problem unique to TanStack Form?
No - similar async validation lifecycle issues appear in Zod's issue tracker and the problem stems from schema validation lifecycle management rather than a single library. The practical fix is defensive: manual safeParseAsync control, pure validators, and re-entrancy guards to harden production forms.
