handleSubmit
A submit handler that waits for validation, hands you parsed values, and routes a validation failure through
onError.
- Category
- Return method
- Signature
handleSubmit(onSubmit, onError?)- Returns
(event?) => Promise<void>
Submit the form without checking the terms box to watch the onError path fire: focus pulls to the broken field and the rejection alert lands. Check the box, fill the email, submit again to see onSubmit receive the parsed values. The dispatch contract section traces every step between the click and the callback.
Signature
const submit = form.handleSubmit(
async (values) => {
/* onSubmit */
},
(errors) => {
/* onError, optional */
}
)
The return value is a function ready for <form @submit>. Call signature: (event?: Event) => Promise<void>. It calls event.preventDefault() for you when it receives the submit event, so bind it with @submit, not @submit.prevent: the .prevent modifier would only prevent the default a second time.
The dispatch contract
When the returned handler fires:
form.meta.submittingflips true for the whole run.- Sync validation runs across every active path.
- Async refinements are awaited.
- If every refinement passes,
onSubmit(values)is called with the parsed Zod output:.transform-aware, fully typed. - The submit counts as a success, flipping
form.meta.submittedtotrue, only whenonSubmitresolves without throwing and leaves no errors set. A callback that hands a server rejection tosetErrorsand returns is a failed submit (see server-side errors). - Every failure pulls focus to the first invalid field. They differ in one respect: a validation failure (step 4 rejected, so
onSubmitnever ran) also callsonError(errors)when supplied. A failure your callback produces after it runs, a throw or asetErrorsand return, does not.onErroris the verdict on Attaform's own validation, and by then that had already passed. A thrown callback additionally lands onform.meta.submitErrorand surfaces onform.errors. form.meta.submissionAttemptsincrements, whichever way the run went.
The counter lands at the end on purpose, which is the one ordering worth holding on to. Inside onSubmit or onError it still reads the count from before this run, so a "retry 2 of 3" affordance built on it inside a callback is off by one; read it after await submit(), or track your own attempt number in the callback. The same lateness is why the default display heuristic reveals every field's errors once the submit settles rather than while it is validating.
While step 4 is awaiting your onSubmit callback, form.meta.submitting is true. It flips back when the callback resolves or rejects. A rejection is caught, not re-thrown, so it never escapes as an unhandled rejection: the raw error lands on form.meta.submitError, and a normalized copy joins form.errors (form-level, or path-scoped when you throw a { path, message }), so a banner reading form.meta.firstOwnError catches it.
Without onError
const submit = form.handleSubmit(async (values) => {
await api.signup(values)
})
Skip onError when the default behavior (focus the first invalid field) is enough. Validation errors still surface through form.errors.<path>; the optional callback is for cross-field UI behavior like a toast or a console log.
Submission state
<button :disabled="form.meta.submitting" type="submit">
{{ form.meta.submitting ? 'Saving…' : 'Save' }}
</button>
form.meta.submitting is the reactive flag while the onSubmit callback runs.
Where to next
errors: the per-path error reads.- When validation runs: the timing knob.
- Display state and showing errors: the
getDisplayStatepredicate.