From a0441e4e38969065c753046f68ec5954e8158ee9 Mon Sep 17 00:00:00 2001 From: Burak Keskin Date: Wed, 19 Aug 2026 20:55:53 +0300 Subject: [PATCH 1/2] fix(form-core): ignore aborted async form-level validation results FieldApi and FormGroupApi already drop in-flight async results after a newer run aborts them. FormApi was missing that check, so a slower previous validator could overwrite newer valid state. --- .changeset/quiet-forms-ignore-stale.md | 5 ++++ packages/form-core/src/FormApi.ts | 1 + packages/form-core/tests/FormApi.spec.ts | 38 ++++++++++++++++++++++++ 3 files changed, 44 insertions(+) create mode 100644 .changeset/quiet-forms-ignore-stale.md diff --git a/.changeset/quiet-forms-ignore-stale.md b/.changeset/quiet-forms-ignore-stale.md new file mode 100644 index 000000000..ed05e7de2 --- /dev/null +++ b/.changeset/quiet-forms-ignore-stale.md @@ -0,0 +1,5 @@ +--- +"@tanstack/form-core": patch +--- + +Ignore aborted async form-level validation results so a slower previous run cannot overwrite newer form state. diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index 4400017d3..3315639d9 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -2232,6 +2232,7 @@ export class FormApi< } catch (e: unknown) { rawError = e as ValidationError } + if (controller.signal.aborted) return resolve(undefined) const { formError, fieldErrors: fieldErrorsFromNormalizeError } = normalizeError(rawError) diff --git a/packages/form-core/tests/FormApi.spec.ts b/packages/form-core/tests/FormApi.spec.ts index c57bb044e..46d18fea4 100644 --- a/packages/form-core/tests/FormApi.spec.ts +++ b/packages/form-core/tests/FormApi.spec.ts @@ -1420,6 +1420,44 @@ describe('form api', () => { }) }) + it('should ignore aborted async form-level validation results', async () => { + vi.useFakeTimers() + + const form = new FormApi({ + defaultValues: { + name: 'valid', + }, + validators: { + onChangeAsyncDebounceMs: 0, + onChangeAsync: async ({ value }) => { + if (value.name === 'slow-invalid') { + await sleep(2000) + return 'Stale validation result' + } + await sleep(50) + return undefined + }, + }, + }) + const field = new FieldApi({ + form, + name: 'name', + }) + form.mount() + field.mount() + + field.setValue('slow-invalid') + await vi.advanceTimersByTimeAsync(0) + field.setValue('valid') + await vi.advanceTimersByTimeAsync(50) + expect(form.state.errors).toEqual([]) + + await vi.advanceTimersByTimeAsync(2000) + expect(form.state.errors).toEqual([]) + expect(form.state.errorMap.onChange).toBeUndefined() + vi.useRealTimers() + }) + it('should run validation onBlur', () => { const form = new FormApi({ defaultValues: { From e7272c9eb99c1b581b9b7a8271aa3020f3c1ffa7 Mon Sep 17 00:00:00 2001 From: Burak Keskin Date: Tue, 20 Oct 2026 16:09:03 +0300 Subject: [PATCH 2/2] Restore real timers even when the aborted validation test fails --- packages/form-core/tests/FormApi.spec.ts | 67 +++++++++++++----------- 1 file changed, 35 insertions(+), 32 deletions(-) diff --git a/packages/form-core/tests/FormApi.spec.ts b/packages/form-core/tests/FormApi.spec.ts index 46d18fea4..318f38e45 100644 --- a/packages/form-core/tests/FormApi.spec.ts +++ b/packages/form-core/tests/FormApi.spec.ts @@ -1423,39 +1423,42 @@ describe('form api', () => { it('should ignore aborted async form-level validation results', async () => { vi.useFakeTimers() - const form = new FormApi({ - defaultValues: { - name: 'valid', - }, - validators: { - onChangeAsyncDebounceMs: 0, - onChangeAsync: async ({ value }) => { - if (value.name === 'slow-invalid') { - await sleep(2000) - return 'Stale validation result' - } - await sleep(50) - return undefined + try { + const form = new FormApi({ + defaultValues: { + name: 'valid', }, - }, - }) - const field = new FieldApi({ - form, - name: 'name', - }) - form.mount() - field.mount() - - field.setValue('slow-invalid') - await vi.advanceTimersByTimeAsync(0) - field.setValue('valid') - await vi.advanceTimersByTimeAsync(50) - expect(form.state.errors).toEqual([]) - - await vi.advanceTimersByTimeAsync(2000) - expect(form.state.errors).toEqual([]) - expect(form.state.errorMap.onChange).toBeUndefined() - vi.useRealTimers() + validators: { + onChangeAsyncDebounceMs: 0, + onChangeAsync: async ({ value }) => { + if (value.name === 'slow-invalid') { + await sleep(2000) + return 'Stale validation result' + } + await sleep(50) + return undefined + }, + }, + }) + const field = new FieldApi({ + form, + name: 'name', + }) + form.mount() + field.mount() + + field.setValue('slow-invalid') + await vi.advanceTimersByTimeAsync(0) + field.setValue('valid') + await vi.advanceTimersByTimeAsync(50) + expect(form.state.errors).toEqual([]) + + await vi.advanceTimersByTimeAsync(2000) + expect(form.state.errors).toEqual([]) + expect(form.state.errorMap.onChange).toBeUndefined() + } finally { + vi.useRealTimers() + } }) it('should run validation onBlur', () => {