Answer to the Core Questions
1. Race condition handling – VeeValidate internally tracks the most recent async validation promise for each field. When a new async rule is triggered (e.g., on input change), the previous promise is aborted via an AbortController. Only the result of the latest promise is applied to the field’s meta object, preventing stale errors from a slower request that finishes after a newer one.
2. Updating meta.valid during pending validation – While an async refinement is pending, meta.valid remains false and meta.pending is set to true. If a form submission occurs in this state, VeeValidate waits for the promise to resolve before deciding the form’s validity. The form will be blocked until the async check completes, ensuring the submission reflects the latest validation result.
Confirmed Behavior (v4.9+, Vue 3)
- Each field’s async rule is wrapped in a cancellable promise.
- Pending state is exposed via
meta.pending (true/false).
- During form submission,
handleSubmit internally calls validateForm, which awaits all pending async validations.
- If the component unmounts while a promise is pending, the controller aborts the promise; the resolved value is ignored and no state mutation occurs.
Practical Steps for a Robust Integration
- Wrap the Zod schema with a custom rule that returns a Promise resolving to an error message or
undefined.
import { z } from 'zod';
import { defineRule } from 'vee-validate';
const asyncZodRule = (schema) => {
return async (value, _, ctx) => {
try {
await schema.parseAsync(value);
return true;
} catch (e) {
return e.errors[0]?.message ?? 'Invalid value';
}
};
};
defineRule('zodAsync', asyncZodRule(z.object({ email: z.string().email() })));
- Initialize the form inside
onMounted to guarantee the reactive proxy exists before any async validation runs.
import { onMounted } from 'vue';
import { useForm } from 'vee-validate';
onMounted(() => {
const { handleSubmit, errors } = useForm({
validationSchema: asyncZodRule(z.object({ email: z.string().email() }))
});
});
- Cancel pending validations on unmount. VeeValidate does this automatically, but if you use a custom async rule that creates its own promise chain, add an explicit
AbortController.
const controller = new AbortController();
const signal = controller.signal;
// Inside the rule:
const promise = schema.parseAsync(value, { signal });
onBeforeUnmount(() => controller.abort());
- Choose the correct
mode for useForm (e.g., blur, change, submit). The mode determines when the async rule is fired and therefore how often meta.pending appears.
useForm({
mode: 'blur', // or 'change', 'submit'
...
});
- Inspect state during rapid input changes. Use Vue DevTools to confirm that only the latest async result updates
errors and meta.valid.
Missing Diagnostic Detail
Could you confirm which mode you are using for useForm (e.g., blur vs change)? This choice affects when async validations are queued and how often meta.pending is true, which in turn influences the perceived race‑condition behavior.
Key Takeaways
- VeeValidate’s internal abort mechanism guarantees that only the most recent async validation result affects the form.
- During form submission, VeeValidate waits for all pending async rules, so the submit button can be disabled until validation resolves.
- Mounting the form inside
onMounted and cancelling pending promises on onBeforeUnmount keeps state updates safe even when the component disappears mid‑validation.