Skip to content

Commit 582589c

Browse files
committed
fix(core): enforce async validator cancellation on rejection
1 parent 639060a commit 582589c

File tree

2 files changed

+46
-8
lines changed

2 files changed

+46
-8
lines changed

packages/core/src/forms/__tests__/form.async-validation.test.ts

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -110,11 +110,17 @@ describe("form.async-validation - utility behavior", () => {
110110
assert.deepEqual(result, { username: "Taken" });
111111
});
112112

113-
test("runAsyncValidation swallows validator rejection", async () => {
114-
const result = await runAsyncValidation({ username: "x" }, async () =>
115-
Promise.reject(new Error("network")),
113+
test("runAsyncValidation wraps validator rejection", async () => {
114+
await assert.rejects(
115+
runAsyncValidation({ username: "x" }, async () => Promise.reject(new Error("network"))),
116+
(error: unknown) => {
117+
assert.ok(error instanceof Error);
118+
assert.equal(error.message, "async form validation failed: network");
119+
assert.ok((error as { cause?: unknown }).cause instanceof Error);
120+
assert.equal(((error as { cause?: unknown }).cause as Error).message, "network");
121+
return true;
122+
},
116123
);
117-
assert.deepEqual(result, {});
118124
});
119125

120126
test("createDebouncedAsyncValidator executes only after debounce window", async () => {
@@ -259,6 +265,36 @@ describe("form.async-validation - utility behavior", () => {
259265
timers.restore();
260266
}
261267
});
268+
269+
test("createDebouncedAsyncValidator ignores rejected in-flight results after cancel", async () => {
270+
const timers = useFakeTimers();
271+
try {
272+
const deferred = createDeferred<ValidationResult<Values>>();
273+
const results: ValidationResult<Values>[] = [];
274+
const errors: unknown[] = [];
275+
const validator = createDebouncedAsyncValidator<Values>(
276+
async () => deferred.promise,
277+
0,
278+
(value) => {
279+
results.push(value);
280+
},
281+
(error) => {
282+
errors.push(error);
283+
},
284+
);
285+
286+
validator.run({ username: "late", email: "" });
287+
timers.tick(0);
288+
validator.cancel();
289+
deferred.reject(new Error("late failure"));
290+
await flushMicrotasks();
291+
292+
assert.equal(results.length, 0);
293+
assert.equal(errors.length, 0);
294+
} finally {
295+
timers.restore();
296+
}
297+
});
262298
});
263299

264300
describe("form.async-validation - useForm behavior", () => {
@@ -422,7 +458,7 @@ describe("form.async-validation - useForm behavior", () => {
422458
assert.equal(form.errors.email, "taken");
423459
});
424460

425-
test("handleSubmit continues when async validation throws network error", async () => {
461+
test("handleSubmit aborts submit when async validation throws network error", async () => {
426462
const h = createFormHarness();
427463
let submitCalls = 0;
428464
const opts = options({
@@ -434,11 +470,13 @@ describe("form.async-validation - useForm behavior", () => {
434470
},
435471
});
436472

437-
const form = h.render(opts);
473+
let form = h.render(opts);
438474
form.handleSubmit();
439475
await flushMicrotasks(4);
476+
form = h.render(opts);
440477

441-
assert.equal(submitCalls, 1);
478+
assert.equal(submitCalls, 0);
479+
assert.equal(form.isSubmitting, false);
442480
});
443481

444482
test("handleSubmit rejects concurrent submits while pending", async () => {

packages/core/src/forms/validation.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ export function createDebouncedAsyncValidator<T extends Record<string, unknown>>
137137
}
138138
})
139139
.catch((e) => {
140-
if (!cancelled && myToken !== token) return;
140+
if (cancelled || myToken !== token) return;
141141
if (onError) {
142142
onError(e);
143143
} else {

0 commit comments

Comments
 (0)