Async validation
Async validators are first-class: any Validator may return a Promise. The form engine aborts stale runs, tracks pending meta, and supports debounce on change.
Writing an async validator
import type { Validator } from "@sometic/validation";
import { createIssue, ok } from "@sometic/validation";
const usernameAvailable: Validator<string> = async (value, context) => {
if (!value) return ok();
const response = await fetch(`/api/username-available?u=${encodeURIComponent(value)}`, {
signal: context.signal,
});
if (context.signal?.aborted) return ok();
const { available } = (await response.json()) as { available: boolean };
return available
? ok()
: createIssue("unavailable", "Username is taken", { path: context.path });
};
form.register("username", {
validators: [required(), usernameAvailable],
validateOn: "onChange",
debounceMs: 300,
});ValidatorContext includes { values, path, signal? }. Pass signal into fetch / SDK calls so abort actually cancels work.
Debounce
- Field / form
debounceMsapplies only when the validation call mode isonChangeanddebounceMs > 0. - Blur and submit validation run immediately (no debounce wait).
- Standalone helper:
import { debouncePromise } from "@sometic/validation";
await debouncePromise((signal) => check(signal), 300, externalSignal);Rejects with AbortError when aborted.
Race and abort handling
Per field, the form keeps an AbortController and a monotonically increasing token:
- A new validation aborts the previous controller.
- Results are ignored if the signal aborted or the token no longer matches.
AbortErrorresolves as a soft failure (false) without throwing into UI.meta.pendingis true while a run is in flight;meta.validrequires!pending.
dispose() / unregister() abort in-flight field validation.
Compose with sync rules
Prefer cheap sync checks first:
import { pipe, required, minLength } from "@sometic/validation";
form.register("username", {
validators: [pipe(required(), minLength(3), usernameAvailable)],
validateOn: "onChange",
debounceMs: 300,
});pipe short-circuits, the network call never runs for empty values. all / runValidators do not short-circuit.
Submit and pending
getFormMeta().pending is true if any field is pending. Submit runs validateForm and waits for field validators (including async) before calling onValid. Avoid treating a form as valid while pending is true.
handleSubmit passes a fresh AbortSignal to onValid(values, { context.signal }) for the submit request itself (separate from field validation signals).
UX guidance
- Show a pending indicator from
meta.pending. - Keep debounce around 200-400ms for uniqueness checks.
- Prefer
onBluror debouncedonChangefor expensive calls; useonSubmitfor final guarantees. - Map API failures to issues with stable codes (
unavailable,rate-limited).